Summary
Implement API for querying PR file changes, diffs, and patches with filtering and analysis capabilities.
Acceptance Criteria
File Query Interface
namespace ConduitUI\Pr\Contracts;
interface FileQueryInterface
{
public function get(): Collection;
public function whereAdded(): Collection;
public function whereModified(): Collection;
public function whereRemoved(): Collection;
public function whereRenamed(): Collection;
public function wherePath(string $pattern): Collection;
public function whereExtension(string $extension): Collection;
public function stats(): FileStats;
}
File Query Implementation
namespace ConduitUI\Pr\Services;
use ConduitUI\Pr\Contracts\FileQueryInterface;
use ConduitUI\Pr\Data\FileChange;
use ConduitUI\Pr\Data\FileStats;
use Illuminate\Support\Str;
final class FileQuery implements FileQueryInterface
{
public function __construct(
protected GitHub $github,
protected string $fullName,
protected int $prNumber,
) {}
public function get(): Collection
{
$response = $this->github->get(
"/repos/{$this->fullName}/pulls/{$this->prNumber}/files"
);
return collect($response->json())
->map(fn($file) => FileChange::fromArray($file));
}
public function whereAdded(): Collection
{
return $this->get()->where('status', 'added');
}
public function whereModified(): Collection
{
return $this->get()->where('status', 'modified');
}
public function whereRemoved(): Collection
{
return $this->get()->where('status', 'removed');
}
public function whereRenamed(): Collection
{
return $this->get()->where('status', 'renamed');
}
public function wherePath(string $pattern): Collection
{
return $this->get()->filter(
fn($file) => Str::is($pattern, $file->filename)
);
}
public function whereExtension(string $extension): Collection
{
return $this->get()->filter(
fn($file) => Str::endsWith($file->filename, ".{$extension}")
);
}
public function stats(): FileStats
{
$files = $this->get();
return new FileStats(
totalFiles: $files->count(),
additions: $files->sum('additions'),
deletions: $files->sum('deletions'),
changes: $files->sum('changes'),
added: $files->where('status', 'added')->count(),
modified: $files->where('status', 'modified')->count(),
removed: $files->where('status', 'removed')->count(),
renamed: $files->where('status', 'renamed')->count(),
);
}
}
File Change DTO
namespace ConduitUI\Pr\Data;
final readonly class FileChange
{
public function __construct(
public string $sha,
public string $filename,
public string $status, // added | modified | removed | renamed
public int $additions,
public int $deletions,
public int $changes,
public ?string $patch,
public ?string $previousFilename,
public string $blobUrl,
public string $rawUrl,
public string $contentsUrl,
) {}
public static function fromArray(array $data): self;
public function isAdded(): bool
{
return $this->status === 'added';
}
public function isModified(): bool
{
return $this->status === 'modified';
}
public function isRemoved(): bool
{
return $this->status === 'removed';
}
public function isRenamed(): bool
{
return $this->status === 'renamed';
}
public function extension(): string
{
return pathinfo($this->filename, PATHINFO_EXTENSION);
}
public function basename(): string
{
return basename($this->filename);
}
public function directory(): string
{
return dirname($this->filename);
}
}
File Stats DTO
namespace ConduitUI\Pr\Data;
final readonly class FileStats
{
public function __construct(
public int $totalFiles,
public int $additions,
public int $deletions,
public int $changes,
public int $added,
public int $modified,
public int $removed,
public int $renamed,
) {}
public function netChange(): int
{
return $this->additions - $this->deletions;
}
public function summary(): string
{
return "{$this->totalFiles} files: +{$this->additions}, -{$this->deletions}";
}
}
Integration with PullRequest DTO
// Add to PullRequest DTO
public function files(): FileQuery
{
return new FileQuery(
$this->github,
$this->fullName,
$this->number
);
}
public function diff(): string
{
$response = $this->github->get(
"/repos/{$this->fullName}/pulls/{$this->number}",
[],
['Accept' => 'application/vnd.github.v3.diff']
);
return $response->body();
}
public function patch(): string
{
$response = $this->github->get(
"/repos/{$this->fullName}/pulls/{$this->number}",
[],
['Accept' => 'application/vnd.github.v3.patch']
);
return $response->body();
}
Usage Examples
Query All Files
$pr = PullRequests::find('owner/repo', 123);
// Get all changed files
$files = $pr->files()->get();
foreach ($files as $file) {
echo "{$file->filename}: +{$file->additions} -{$file->deletions}\n";
}
Filter Files by Status
// New files
$newFiles = $pr->files()->whereAdded();
// Modified files
$modified = $pr->files()->whereModified();
// Deleted files
$deleted = $pr->files()->whereRemoved();
Filter by Path Pattern
// All PHP files
$phpFiles = $pr->files()->whereExtension('php');
// Files in src directory
$srcFiles = $pr->files()->wherePath('src/*');
// Test files
$tests = $pr->files()->wherePath('tests/**/*.php');
// Configuration files
$configs = $pr->files()->wherePath('config/*.php');
File Statistics
$stats = $pr->files()->stats();
echo $stats->summary();
// Output: "12 files: +234, -45"
echo "Net change: {$stats->netChange()} lines";
// Output: "Net change: 189 lines"
echo "Files modified: {$stats->modified}";
Get Raw Diff
$pr = PullRequests::find('owner/repo', 123);
// Get unified diff
$diff = $pr->diff();
file_put_contents('pr-123.diff', $diff);
// Get patch format
$patch = $pr->patch();
file_put_contents('pr-123.patch', $patch);
Automated Checks Based on Files
$pr = PullRequests::find('owner/repo', 123);
// Check if migrations were added without tests
$migrations = $pr->files()->wherePath('database/migrations/*.php');
$tests = $pr->files()->wherePath('tests/**/*Test.php');
if ($migrations->isNotEmpty() && $tests->isEmpty()) {
$pr->comment('⚠️ Migration detected but no tests added.');
}
// Check for large PRs
$stats = $pr->files()->stats();
if ($stats->totalFiles > 20 || $stats->changes > 500) {
$pr->comment('⚠️ Large PR detected. Consider breaking into smaller PRs.');
}
Require Tests for Source Changes
$pr = PullRequests::find('owner/repo', 123);
$sourceChanged = $pr->files()
->wherePath('src/**/*.php')
->isNotEmpty();
$testsChanged = $pr->files()
->wherePath('tests/**/*.php')
->isNotEmpty();
if ($sourceChanged && !$testsChanged) {
$pr->requestChanges('Please add tests for source code changes')
->submit();
}
Documentation Check
$pr = PullRequests::find('owner/repo', 123);
$docsChanged = $pr->files()
->wherePath('docs/**/*.md')
->isNotEmpty();
$readmeChanged = $pr->files()
->wherePath('README.md')
->isNotEmpty();
if (!$docsChanged && !$readmeChanged) {
$pr->comment('💡 Consider updating documentation for these changes.');
}
Analyze File Types
$pr = PullRequests::find('owner/repo', 123);
$files = $pr->files()->get();
$byExtension = $files->groupBy(fn($file) => $file->extension());
foreach ($byExtension as $ext => $files) {
echo "{$ext}: {$files->count()} files\n";
}
Testing Requirements
it('gets all changed files')
->expect(fn() =>
PullRequests::find('test/repo', 1)->files()->get()
)->toBeInstanceOf(Collection::class);
it('filters added files')
->expect(fn() =>
PullRequests::find('test/repo', 1)->files()->whereAdded()
)->toBeInstanceOf(Collection::class);
it('filters by extension')
->expect(fn() =>
PullRequests::find('test/repo', 1)->files()->whereExtension('php')
)->toBeInstanceOf(Collection::class);
it('generates file statistics')
->expect(fn() =>
PullRequests::find('test/repo', 1)->files()->stats()
)->toBeInstanceOf(FileStats::class);
it('retrieves diff')
->expect(fn() =>
PullRequests::find('test/repo', 1)->diff()
)->toBeString();
Dependencies
References
Labels
Summary
Implement API for querying PR file changes, diffs, and patches with filtering and analysis capabilities.
Acceptance Criteria
File Query Interface
File Query Implementation
File Change DTO
File Stats DTO
Integration with PullRequest DTO
Usage Examples
Query All Files
Filter Files by Status
Filter by Path Pattern
File Statistics
Get Raw Diff
Automated Checks Based on Files
Require Tests for Source Changes
Documentation Check
Analyze File Types
Testing Requirements
Dependencies
References
Labels