<?php
declare(strict_types=1);
namespace Butschster\ContextGenerator\Source\Gitlab;
use Butschster\ContextGenerator\Config\Exclude\ExcludeRegistryInterface;
use Butschster\ContextGenerator\Lib\Finder\FinderInterface;
use Butschster\ContextGenerator\Lib\Finder\FinderResult;
use Butschster\ContextGenerator\Lib\GitlabClient\GitlabClientInterface;
use Butschster\ContextGenerator\Lib\GitlabClient\Model\GitlabRepository;
use Butschster\ContextGenerator\Lib\PathFilter\ContentsFilter;
use Butschster\ContextGenerator\Lib\PathFilter\ExcludePathFilter;
use Butschster\ContextGenerator\Lib\PathFilter\FilePatternFilter;
use Butschster\ContextGenerator\Lib\PathFilter\FilterInterface;
use Butschster\ContextGenerator\Lib\PathFilter\PathFilter;
use Butschster\ContextGenerator\Lib\TreeBuilder\FileTreeBuilder;
use Butschster\ContextGenerator\Lib\Variable\VariableResolver;
use Butschster\ContextGenerator\Source\Fetcher\FilterableSourceInterface;
use Butschster\ContextGenerator\Application\Logger\LoggerPrefix;
use Psr\Log\LoggerInterface;
final class GitlabFinder implements FinderInterface
{
/**
* Filters to apply
*
* @var array<FilterInterface>
*/
private array $filters = [];
public function __construct(
private readonly GitlabClientInterface $gitlabClient,
private readonly ExcludeRegistryInterface $excludeRegistry,
private readonly VariableResolver $variableResolver = new VariableResolver(),
private readonly FileTreeBuilder $fileTreeBuilder = new FileTreeBuilder(),
#[LoggerPrefix(prefix: 'gitlab-finder')]
private readonly ?LoggerInterface $logger = null,
) {}
public function find(FilterableSourceInterface $source, string $basePath = '', array $options = []): FinderResult
{
if (!$source instanceof GitlabSource) {
throw new \InvalidArgumentException('Source must be an instance of GitlabSource');
}
// Configure GitLab client based on source configuration
$this->configureGitlabClient($source);
// Parse repository from string
$repository = new GitlabRepository($source->repository, $source->branch);
// Initialize path filters based on source configuration
$this->initializePathFilters($source);
// Get source paths
$sourcePaths = $source->sourcePaths;
if (\is_string($sourcePaths)) {
$sourcePaths = [$sourcePaths];
}
// Recursively discover all files from repository paths
$discoveredItems = $this->discoverRepositoryItems($repository, $sourcePaths);
// Apply path-based filters
$filteredItems = $this->applyFilters($discoveredItems);
// Build result structure
$files = [];
$this->buildResultStructure($filteredItems, $repository, $files);
// Apply content filters
$files = (new ContentsFilter(
contains: $source->contains(),
notContains: $source->notContains(),
))->apply($files);
// Apply global exclusion registry
$files = $this->applyGlobalExclusions($files);
/** @psalm-suppress InvalidArgument */
$tree = \array_map(static fn(GitlabFileInfo $file): string => $file->getRelativePathname(), $files);
// Create the result
return new FinderResult(
\array_values($files),
$this->fileTreeBuilder->buildTree($tree, '', $options),
);
}
public function applyFilters(array $items): array
{
foreach ($this->filters as $filter) {
$items = $filter->apply($items);
}
return $items;
}
/**
* Apply global exclusion patterns to filter files
*/
private function applyGlobalExclusions(array $files): array
{
return \array_filter($files, function (GitlabFileInfo $file): bool {
$path = $file->getRelativePathname();
if ($this->excludeRegistry->shouldExclude($path)) {
$this->logger?->debug('File excluded by global exclusion pattern', [
'path' => $path,
]);
return false;
}
return true;
});
}
/**
* Configure the GitLab client based on source configuration
*/
private function configureGitlabClient(GitlabSource $source): void
{
$serverConfig = $source->server;
$resolvedConfig = $serverConfig->withResolvedVariables($this->variableResolver);
$this->gitlabClient->setServerUrl($resolvedConfig->url);
if ($resolvedConfig->token !== null) {
$this->gitlabClient->setToken($resolvedConfig->token);
}
if (!empty($resolvedConfig->headers)) {
$this->gitlabClient->setHeaders($resolvedConfig->headers);
}
}
/**
* Initialize path filters based on source configuration
*
* @param FilterableSourceInterface $source Source with filter criteria
*/
private function initializePathFilters(FilterableSourceInterface $source): void
{
// Clear existing filters
$this->filters = [];
// Add file name pattern filter
$filePattern = $source->name();
if ($filePattern) {
$this->filters[] = new FilePatternFilter($filePattern);
}
// Add path inclusion filter
$path = $source->path();
if ($path) {
$this->filters[] = new PathFilter($path);
}
// Add path exclusion filter
$excludePatterns = $source->notPath();
if ($excludePatterns) {
$this->filters[] = new ExcludePathFilter($excludePatterns);
}
}
/**
* Discover all items from repository paths recursively
*
* @param GitlabRepository $repository GitLab repository
* @param array<string> $sourcePaths Source paths to discover
* @return array<array<string, mixed>> Discovered items
*/
private function discoverRepositoryItems(GitlabRepository $repository, array $sourcePaths): array
{
$allItems = [];
foreach ($sourcePaths as $path) {
$items = $this->fetchDirectoryContents($repository, $path);
$allItems = \array_merge($allItems, $this->traverseDirectoryRecursively($items, $repository));
}
return $allItems;
}
/**
* Traverse directory items recursively to discover all files
*/
private function traverseDirectoryRecursively(array $items, GitlabRepository $repository): array
{
$result = [];
foreach ($items as $item) {
if (($item['type'] ?? '') === 'tree') {
$subItems = $this->fetchDirectoryContents($repository, $item['path']);
$result = \array_merge($result, $this->traverseDirectoryRecursively($subItems, $repository));
} else {
$result[] = $item;
}
}
return $result;
}
/**
* Build the final result structure (files and tree)
*/
private function buildResultStructure(
array $items,
GitlabRepository $repository,
array &$files,
): void {
foreach ($items as $item) {
$path = $item['path'];
try {
$relativePath = \dirname((string) $path);
if ($relativePath === '.') {
$relativePath = '';
}
// Add to files array
$files[] = new GitlabFileInfo(
$relativePath,
$path,
$item,
fn() => $this->fetchFileContent($repository, $path),
);
} catch (\Exception) {
// Skip files that can't be processed
continue;
}
}
}
/**
* Fetch directory contents from GitLab API
*/
private function fetchDirectoryContents(GitlabRepository $repository, string $path = ''): array
{
return $this->gitlabClient->getContents($repository, $path);
}
/**
* Fetch file content from GitLab API
*/
private function fetchFileContent(GitlabRepository $repository, string $path): string
{
return $this->gitlabClient->getFileContent($repository, $path);
}
}