| | <?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'; |
| | +require_once __DIR__ . '/RepositoryList.php'; |
| | +require_once __DIR__ . '/git/Git.php'; |
| | +require_once __DIR__ . '/pages/CommitsPage.php'; |
| | +require_once __DIR__ . '/pages/DiffPage.php'; |
| | +require_once __DIR__ . '/pages/HomePage.php'; |
| | +require_once __DIR__ . '/pages/FilePage.php'; |
| | +require_once __DIR__ . '/pages/RawPage.php'; |
| | +require_once __DIR__ . '/pages/TagsPage.php'; |
| | +require_once __DIR__ . '/pages/ClonePage.php'; |
| | +require_once __DIR__ . '/pages/ComparePage.php'; |
| | |
| | - private string $reposPath; |
| | +class Router { |
| | + private const ACTION_TREE = 'tree'; |
| | + private const ACTION_BLOB = 'blob'; |
| | + private const ACTION_RAW = 'raw'; |
| | + private const ACTION_COMMITS = 'commits'; |
| | + private const ACTION_COMMIT = 'commit'; |
| | + private const ACTION_COMPARE = 'compare'; |
| | + private const ACTION_TAGS = 'tags'; |
| | |
| | - public function __construct( string $path ) { |
| | - $this->reposPath = $path; |
| | - } |
| | + private const GET_REPOSITORY = 'repo'; |
| | + private const GET_ACTION = 'action'; |
| | + private const GET_HASH = 'hash'; |
| | + private const GET_NAME = 'name'; |
| | |
| | - public function eachRepository( callable $callback ): void { |
| | - $repos = $this->sortRepositories( $this->loadRepositories() ); |
| | + private const REFERENCE_HEAD = 'HEAD'; |
| | + private const ROUTE_REPO = 'repo'; |
| | + private const EXTENSION_GIT = '.git'; |
| | |
| | - foreach( $repos as $repo ) { |
| | - $callback( $repo ); |
| | - } |
| | - } |
| | + private array $repos = []; |
| | + private Git $git; |
| | |
| | - private function loadRepositories(): array { |
| | - $repos = []; |
| | - $path = $this->reposPath . self::GLOB_PATTERN; |
| | - $dirs = glob( $path, GLOB_ONLYDIR ); |
| | + private string $repoName = ''; |
| | + private array $repoData = []; |
| | + private string $action = ''; |
| | + private string $commitHash = ''; |
| | + private string $filePath = ''; |
| | + private string $baseHash = ''; |
| | |
| | - if( $dirs !== false ) { |
| | - $repos = $this->processDirectories( $dirs ); |
| | - } |
| | + public function __construct( string $reposPath ) { |
| | + $this->git = new Git( $reposPath ); |
| | + $list = new RepositoryList( $reposPath ); |
| | |
| | - return $repos; |
| | + $list->eachRepository( function( $repo ) { |
| | + $this->repos[$repo['safe_name']] = $repo; |
| | + }); |
| | } |
| | - |
| | - private function processDirectories( array $dirs ): array { |
| | - $repos = []; |
| | |
| | - foreach( $dirs as $dir ) { |
| | - $data = $this->createRepositoryData( $dir ); |
| | + public function route(): Page { |
| | + $this->normalizeQueryString(); |
| | + $uriParts = $this->parseUriParts(); |
| | + $repoName = !empty( $uriParts ) ? array_shift( $uriParts ) : ''; |
| | + $page = new HomePage( $this->repos, $this->git ); |
| | |
| | - if( $data !== [] ) { |
| | - $repos[$data[self::KEY_NAME]] = $data; |
| | + if( $repoName !== '' ) { |
| | + if( str_ends_with( $repoName, self::EXTENSION_GIT ) ) { |
| | + $page = $this->handleCloneRoute( $repoName, $uriParts ); |
| | + } elseif( isset( $this->repos[$repoName] ) ) { |
| | + $page = $this->resolveActionRoute( $repoName, $uriParts ); |
| | } |
| | } |
| | |
| | - return $repos; |
| | + return $page; |
| | } |
| | |
| | - private function createRepositoryData( string $dir ): array { |
| | - $data = []; |
| | - $base = basename( $dir ); |
| | + private function handleCloneRoute( |
| | + string $repoName, |
| | + array $uriParts |
| | + ): Page { |
| | + $realName = substr( $repoName, 0, -4 ); |
| | + $path = ''; |
| | |
| | - if( $base[0] !== self::HIDDEN_PREFIX ) { |
| | - $name = $this->extractName( $base ); |
| | - $data = [ |
| | - self::KEY_NAME => $name, |
| | - self::KEY_SAFE_NAME => $name, |
| | - self::KEY_PATH => $dir, |
| | - ]; |
| | + if( isset( $this->repos[$realName]['path'] ) ) { |
| | + $path = $this->repos[$realName]['path']; |
| | + } elseif( isset( $this->repos[$repoName]['path'] ) ) { |
| | + $path = $this->repos[$repoName]['path']; |
| | } |
| | - |
| | - 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 ); |
| | + if( $path === '' ) { |
| | + http_response_code( 404 ); |
| | + exit( "Repository not found" ); |
| | } |
| | - |
| | - 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 ); |
| | - } |
| | + $this->git->setRepository( $path ); |
| | |
| | - return $repos; |
| | + return new ClonePage( $this->git, implode( '/', $uriParts ) ); |
| | } |
| | |
| | - private function applyCustomOrder( array $repos, string $file ): array { |
| | - $lines = file( $file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ); |
| | + private function resolveActionRoute( |
| | + string $repoName, |
| | + array $uriParts |
| | + ): Page { |
| | + $this->repoData = $this->repos[$repoName]; |
| | + $this->repoName = $repoName; |
| | |
| | - if( $lines !== false ) { |
| | - $config = $this->parseOrderFile( $lines ); |
| | - $repos = $this->filterExcluded( $repos, $config[self::KEY_EXCLUDE] ); |
| | - $repos = $this->sortWithConfig( $repos, $config[self::KEY_ORDER] ); |
| | - } |
| | + $this->git->setRepository( $this->repoData['path'] ); |
| | |
| | - return $repos; |
| | - } |
| | + $act = array_shift( $uriParts ); |
| | + $this->action = $act ?: self::ACTION_TREE; |
| | |
| | - private function parseOrderFile( array $lines ): array { |
| | - $order = []; |
| | - $exclude = []; |
| | + $this->commitHash = self::REFERENCE_HEAD; |
| | + $this->filePath = ''; |
| | + $this->baseHash = ''; |
| | |
| | - foreach( $lines as $line ) { |
| | - $trim = trim( $line ); |
| | + $hasHash = [ |
| | + self::ACTION_TREE, self::ACTION_BLOB, self::ACTION_RAW, |
| | + self::ACTION_COMMITS |
| | + ]; |
| | |
| | - if( $trim !== '' ) { |
| | - if( str_starts_with( $trim, self::EXCLUDE_CHAR ) ) { |
| | - $exclude = $this->addExclusion( $exclude, $trim ); |
| | - } else { |
| | - $order[$trim] = count( $order ); |
| | - } |
| | - } |
| | + if( in_array( $this->action, $hasHash ) ) { |
| | + $hash = array_shift( $uriParts ); |
| | + $this->commitHash = $hash ?: self::REFERENCE_HEAD; |
| | + $this->filePath = implode( '/', $uriParts ); |
| | + } elseif( $this->action === self::ACTION_COMMIT ) { |
| | + $this->commitHash = array_shift( $uriParts ); |
| | + } elseif( $this->action === self::ACTION_COMPARE ) { |
| | + $this->commitHash = array_shift( $uriParts ); |
| | + $this->baseHash = array_shift( $uriParts ); |
| | } |
| | |
| | - return [ self::KEY_ORDER => $order, self::KEY_EXCLUDE => $exclude ]; |
| | - } |
| | + $this->populateGet(); |
| | |
| | - private function addExclusion( array $exclude, string $line ): array { |
| | - $name = substr( $line, 1 ); |
| | - $exclude[$name] = true; |
| | + return $this->createPage(); |
| | + } |
| | |
| | - return $exclude; |
| | + private function createPage(): Page { |
| | + return match( $this->action ) { |
| | + self::ACTION_TREE, |
| | + self::ACTION_BLOB => new FilePage( |
| | + $this->repos, $this->repoData, $this->git, $this->commitHash, |
| | + $this->filePath |
| | + ), |
| | + self::ACTION_RAW => new RawPage( |
| | + $this->git, $this->commitHash |
| | + ), |
| | + self::ACTION_COMMITS => new CommitsPage( |
| | + $this->repos, $this->repoData, $this->git, $this->commitHash |
| | + ), |
| | + self::ACTION_COMMIT => new DiffPage( |
| | + $this->repos, $this->repoData, $this->git, $this->commitHash |
| | + ), |
| | + self::ACTION_TAGS => new TagsPage( |
| | + $this->repos, $this->repoData, $this->git |
| | + ), |
| | + self::ACTION_COMPARE => new ComparePage( |
| | + $this->repos, $this->repoData, $this->git, $this->commitHash, |
| | + $this->baseHash |
| | + ), |
| | + default => new FilePage( |
| | + $this->repos, $this->repoData, $this->git, self::REFERENCE_HEAD, '' |
| | + ) |
| | + }; |
| | } |
| | |
| | - private function filterExcluded( array $repos, array $exclude ): array { |
| | - foreach( $repos as $key => $repo ) { |
| | - if( isset( $exclude[$repo[self::KEY_SAFE_NAME]] ) ) { |
| | - unset( $repos[$key] ); |
| | - } |
| | + private function normalizeQueryString(): void { |
| | + if( empty( $_GET ) && !empty( $_SERVER['QUERY_STRING'] ) ) { |
| | + parse_str( $_SERVER['QUERY_STRING'], $_GET ); |
| | } |
| | - |
| | - 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 ); |
| | - } ); |
| | + private function parseUriParts(): array { |
| | + $requestUri = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ); |
| | + $scriptName = dirname( $_SERVER['SCRIPT_NAME'] ); |
| | |
| | - return $repos; |
| | - } |
| | + if( $scriptName !== '/' && strpos( $requestUri, $scriptName ) === 0 ) { |
| | + $requestUri = substr( $requestUri, strlen( $scriptName ) ); |
| | + } |
| | |
| | - 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; |
| | + $requestUri = trim( $requestUri, '/' ); |
| | + $uriParts = explode( '/', $requestUri ); |
| | |
| | - $result = $posA === $posB |
| | - ? strcasecmp( $safeA, $safeB ) |
| | - : $posA <=> $posB; |
| | + if( !empty( $uriParts ) && $uriParts[0] === self::ROUTE_REPO ) { |
| | + array_shift( $uriParts ); |
| | + } |
| | |
| | - return $result; |
| | + return $uriParts; |
| | + } |
| | + |
| | + private function populateGet(): void { |
| | + $_GET[self::GET_REPOSITORY] = $this->repoName; |
| | + $_GET[self::GET_ACTION] = $this->action; |
| | + $_GET[self::GET_HASH] = $this->commitHash; |
| | + $_GET[self::GET_NAME] = $this->filePath; |
| | } |
| | } |