Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git
<?php
class RepositoryList {
  private const GIT_EXT       = '.git';
  private const ORDER_FILE    = '/order.txt';
  private const GLOB_PATTERN  = '/*';
  private const HIDDEN_PREFIX = '.';
  private const EXCLUDE_CHAR  = '-';
  private const SORT_MAX      = PHP_INT_MAX;

  private const KEY_SAFE_NAME = 'safe_name';
  private const KEY_EXCLUDE   = 'exclude';
  private const KEY_ORDER     = 'order';
  private const KEY_PATH      = 'path';
  private const KEY_NAME      = 'name';

  private string $reposPath;

  public function __construct( string $path ) {
    $this->reposPath = $path;
  }

  public function eachRepository( callable $callback ): void {
    $repos = $this->sortRepositories( $this->loadRepositories() );

    foreach( $repos as $repo ) {
      $callback( $repo );
    }
  }

  private function loadRepositories(): array {
    $repos = [];
    $path  = $this->reposPath . self::GLOB_PATTERN;
    $dirs  = glob( $path, GLOB_ONLYDIR );

    if( $dirs !== false ) {
      $repos = $this->processDirectories( $dirs );
    }

    return $repos;
  }

  private function processDirectories( array $dirs ): array {
    $repos = [];

    foreach( $dirs as $dir ) {
      $data = $this->createRepositoryData( $dir );

      if( $data !== [] ) {
        $repos[$data[self::KEY_NAME]] = $data;
      }
    }

    return $repos;
  }

  private function createRepositoryData( string $dir ): array {
    $data = [];
    $base = basename( $dir );

    if( $base[0] !== self::HIDDEN_PREFIX ) {
      $name = $this->extractName( $base );
      $data = [
        self::KEY_NAME      => $name,
        self::KEY_SAFE_NAME => $name,
        self::KEY_PATH      => $dir,
      ];
    }

    return $data;
  }

  private function extractName( string $base ): string {
    $name = $base;

    if( str_ends_with( $base, self::GIT_EXT ) ) {
      $len  = strlen( self::GIT_EXT );
      $name = substr( $base, 0, -$len );
    }

    return $name;
  }

  private function sortRepositories( array $repos ): array {
    $file = __DIR__ . self::ORDER_FILE;

    if( file_exists( $file ) ) {
      $repos = $this->applyCustomOrder( $repos, $file );
    } else {
      ksort( $repos, SORT_NATURAL | SORT_FLAG_CASE );
    }

    return $repos;
  }

  private function applyCustomOrder( array $repos, string $file ): array {
    $lines = file( $file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );

    if( $lines !== false ) {
      $config = $this->parseOrderFile( $lines );
      $repos  = $this->filterExcluded( $repos, $config[self::KEY_EXCLUDE] );
      $repos  = $this->sortWithConfig( $repos, $config[self::KEY_ORDER] );
    }

    return $repos;
  }

  private function parseOrderFile( array $lines ): array {
    $order   = [];
    $exclude = [];

    foreach( $lines as $line ) {
      $trim = trim( $line );

      if( $trim !== '' ) {
        if( str_starts_with( $trim, self::EXCLUDE_CHAR ) ) {
          $exclude = $this->addExclusion( $exclude, $trim );
        } else {
          $order[$trim] = count( $order );
        }
      }
    }

    return [ self::KEY_ORDER => $order, self::KEY_EXCLUDE => $exclude ];
  }

  private function addExclusion( array $exclude, string $line ): array {
    $name           = substr( $line, 1 );
    $exclude[$name] = true;

    return $exclude;
  }

  private function filterExcluded( array $repos, array $exclude ): array {
    foreach( $repos as $key => $repo ) {
      if( isset( $exclude[$repo[self::KEY_SAFE_NAME]] ) ) {
        unset( $repos[$key] );
      }
    }

    return $repos;
  }

  private function sortWithConfig( array $repos, array $order ): array {
    uasort( $repos, function( array $repoA, array $repoB ) use( $order ): int {
      return $this->compareRepositories( $repoA, $repoB, $order );
    } );

    return $repos;
  }

  private function compareRepositories(
    array $repoA,
    array $repoB,
    array $order
  ): int {
    $safeA = $repoA[self::KEY_SAFE_NAME];
    $safeB = $repoB[self::KEY_SAFE_NAME];
    $posA  = $order[$safeA] ?? self::SORT_MAX;
    $posB  = $order[$safeB] ?? self::SORT_MAX;

    $result = $posA === $posB
      ? strcasecmp( $safeA, $safeB )
      : $posA <=> $posB;

    return $result;
  }
}