|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +namespace Butschster\ContextGenerator\Fetcher; |
| 6 | + |
| 7 | +use Butschster\ContextGenerator\Fetcher\Finder\FinderResult; |
| 8 | +use Butschster\ContextGenerator\Fetcher\Finder\CommitDiffFinder; |
| 9 | +use Butschster\ContextGenerator\Fetcher\Git\CommitRangeParser; |
| 10 | +use Butschster\ContextGenerator\Source\CommitDiffSource; |
| 11 | +use Butschster\ContextGenerator\Source\SourceModifierRegistry; |
| 12 | +use Butschster\ContextGenerator\SourceInterface; |
| 13 | +use Symfony\Component\Finder\SplFileInfo; |
| 14 | + |
| 15 | +/** |
| 16 | + * Fetcher for git commit diffs |
| 17 | + * @implements SourceFetcherInterface<CommitDiffSource> |
| 18 | + */ |
| 19 | +final readonly class CommitDiffSourceFetcher implements SourceFetcherInterface |
| 20 | +{ |
| 21 | + /** |
| 22 | + * @param SourceModifierRegistry $modifiers Registry of content modifiers |
| 23 | + * @param FinderInterface $finder Finder for filtering diffs |
| 24 | + * @param CommitRangeParser $rangeParser Parser for commit range expressions |
| 25 | + */ |
| 26 | + public function __construct( |
| 27 | + private SourceModifierRegistry $modifiers, |
| 28 | + private CommitRangeParser $rangeParser = new CommitRangeParser(), |
| 29 | + private FinderInterface $finder = new CommitDiffFinder(), |
| 30 | + ) {} |
| 31 | + |
| 32 | + public function supports(SourceInterface $source): bool |
| 33 | + { |
| 34 | + return $source instanceof CommitDiffSource; |
| 35 | + } |
| 36 | + |
| 37 | + public function fetch(SourceInterface $source): string |
| 38 | + { |
| 39 | + if (!$source instanceof CommitDiffSource) { |
| 40 | + throw new \InvalidArgumentException('Source must be an instance of CommitDiffSource'); |
| 41 | + } |
| 42 | + |
| 43 | + // Ensure the repository exists |
| 44 | + if (!\is_dir($source->repository)) { |
| 45 | + throw new \RuntimeException(\sprintf('Git repository "%s" does not exist', $source->repository)); |
| 46 | + } |
| 47 | + |
| 48 | + // Parse and resolve the commit range |
| 49 | + $resolvedCommitRange = $this->rangeParser->resolve($source->getCommit()); |
| 50 | + |
| 51 | + // Use the finder to get the diffs (passing the resolved range) |
| 52 | + $finderResult = $this->findDiffs($source, $resolvedCommitRange); |
| 53 | + |
| 54 | + // Extract diffs from the finder result |
| 55 | + $diffs = $this->extractDiffsFromFinderResult($finderResult); |
| 56 | + |
| 57 | + // Format the output |
| 58 | + return $this->formatOutput($diffs, $finderResult->treeView, $source, $resolvedCommitRange); |
| 59 | + } |
| 60 | + |
| 61 | + /** |
| 62 | + * Find diffs for the given source and commit range |
| 63 | + */ |
| 64 | + private function findDiffs(CommitDiffSource $source, string|array $commitRange): FinderResult |
| 65 | + { |
| 66 | + // Create a source with the resolved commit range to pass to the finder |
| 67 | + $finderSource = new class($source, $commitRange) extends CommitDiffSource { |
| 68 | + public function __construct(CommitDiffSource $original, private readonly string|array $resolvedCommitRange) |
| 69 | + { |
| 70 | + parent::__construct( |
| 71 | + repository: $original->repository, |
| 72 | + description: $original->getDescription(), |
| 73 | + commit: $original->commit, |
| 74 | + filePattern: $original->filePattern, |
| 75 | + notPath: $original->notPath, |
| 76 | + path: $original->path, |
| 77 | + contains: $original->contains, |
| 78 | + notContains: $original->notContains, |
| 79 | + showStats: $original->showStats, |
| 80 | + ); |
| 81 | + } |
| 82 | + |
| 83 | + public function getCommitRange(): string|array |
| 84 | + { |
| 85 | + return $this->resolvedCommitRange; |
| 86 | + } |
| 87 | + }; |
| 88 | + |
| 89 | + return $this->finder->find($finderSource); |
| 90 | + } |
| 91 | + |
| 92 | + /** |
| 93 | + * Extract diffs from the finder result |
| 94 | + * |
| 95 | + * @return array<string, array{file: string, diff: string, stats: string}> |
| 96 | + */ |
| 97 | + private function extractDiffsFromFinderResult(FinderResult $finderResult): array |
| 98 | + { |
| 99 | + $diffs = []; |
| 100 | + foreach ($finderResult->files as $file) { |
| 101 | + if (!$file instanceof SplFileInfo) { |
| 102 | + continue; |
| 103 | + } |
| 104 | + |
| 105 | + // Get the original path and diff content |
| 106 | + $originalPath = \method_exists($file, 'getOriginalPath') |
| 107 | + ? $file->getOriginalPath() |
| 108 | + : $file->getRelativePathname(); |
| 109 | + |
| 110 | + $diffContent = $file->getContents(); |
| 111 | + |
| 112 | + // Get the stats for this file |
| 113 | + $stats = ''; |
| 114 | + if (\method_exists($file, 'getStats')) { |
| 115 | + $stats = $file->getStats(); |
| 116 | + } else { |
| 117 | + // Try to extract stats from the diff content |
| 118 | + \preg_match('/^(.*?)(?=diff --git)/s', $diffContent, $matches); |
| 119 | + if (!empty($matches[1])) { |
| 120 | + $stats = \trim($matches[1]); |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + $diffs[$originalPath] = [ |
| 125 | + 'file' => $originalPath, |
| 126 | + 'diff' => $diffContent, |
| 127 | + 'stats' => $stats, |
| 128 | + ]; |
| 129 | + } |
| 130 | + |
| 131 | + return $diffs; |
| 132 | + } |
| 133 | + |
| 134 | + /** |
| 135 | + * Format the diffs for output |
| 136 | + * |
| 137 | + * @param array<string, array{file: string, diff: string, stats: string}> $diffs |
| 138 | + */ |
| 139 | + private function formatOutput( |
| 140 | + array $diffs, |
| 141 | + string $treeView, |
| 142 | + CommitDiffSource $source, |
| 143 | + string|array $resolvedCommitRange, |
| 144 | + ): string { |
| 145 | + $content = ''; |
| 146 | + |
| 147 | + // Handle empty diffs case |
| 148 | + if (empty($diffs)) { |
| 149 | + $formattedRange = $this->rangeParser->formatForDisplay($resolvedCommitRange); |
| 150 | + return "# Git Diff for Commit Range: {$formattedRange}\n\nNo changes found in this commit range.\n"; |
| 151 | + } |
| 152 | + |
| 153 | + // Add a header with the commit range |
| 154 | + $formattedRange = $this->rangeParser->formatForDisplay($resolvedCommitRange); |
| 155 | + $content .= "# Git Diff for Commit Range: {$formattedRange}\n\n"; |
| 156 | + |
| 157 | + // Add a tree view summary of changed files |
| 158 | + $content .= "## Summary of Changes\n\n"; |
| 159 | + $content .= "```\n"; |
| 160 | + $content .= $treeView; |
| 161 | + $content .= "```\n\n"; |
| 162 | + |
| 163 | + // Add each diff |
| 164 | + foreach ($diffs as $file => $diffData) { |
| 165 | + // Add stats if requested |
| 166 | + if ($source->showStats && !empty($diffData['stats'])) { |
| 167 | + $content .= "## Stats for {$file}\n\n"; |
| 168 | + $content .= "```\n{$diffData['stats']}\n```\n\n"; |
| 169 | + } |
| 170 | + |
| 171 | + // Add the diff |
| 172 | + $content .= "## Diff for {$file}\n\n"; |
| 173 | + $content .= "```diff\n{$diffData['diff']}\n```\n\n"; |
| 174 | + |
| 175 | + // Apply modifiers if available |
| 176 | + if (!empty($source->modifiers)) { |
| 177 | + foreach ($source->modifiers as $modifierId) { |
| 178 | + if ($this->modifiers->has($modifierId)) { |
| 179 | + $modifier = $this->modifiers->get($modifierId); |
| 180 | + if ($modifier->supports($file)) { |
| 181 | + $context = [ |
| 182 | + 'file' => $file, |
| 183 | + 'source' => $source, |
| 184 | + ]; |
| 185 | + $content = $modifier->modify($content, $context); |
| 186 | + } |
| 187 | + } |
| 188 | + } |
| 189 | + } |
| 190 | + } |
| 191 | + |
| 192 | + return $content; |
| 193 | + } |
| 194 | +} |
0 commit comments