Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 106 additions & 10 deletions src/Struct/Local/LocalPullRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,30 @@ public function getFiles(): FileCollection
return $this->files;
}

$process = new Process([
'git',
'diff',
$this->target . '..' . $this->local,
'--name-status',
$diffRange = $this->target . '..' . $this->local;

$nameStatusProcess = new Process([
'git', 'diff', $diffRange, '--name-status',
], $this->repo);

$process->mustRun();
$numstatProcess = new Process([
'git', 'diff', $diffRange, '--numstat',
], $this->repo);

$patchProcess = new Process([
'git', 'diff', $diffRange,
], $this->repo);

$nameStatusProcess->mustRun();
$numstatProcess->mustRun();
$patchProcess->mustRun();

$numstats = $this->parseNumstat($numstatProcess->getOutput());
$patches = $this->parsePatch($patchProcess->getOutput());

$files = new FileCollection();

foreach (explode(\PHP_EOL, $process->getOutput()) as $line) {
foreach (explode(\PHP_EOL, $nameStatusProcess->getOutput()) as $line) {
if ($line === '') {
continue;
}
Expand All @@ -111,9 +123,10 @@ public function getFiles(): FileCollection

$element = new LocalFile($this->repo . '/' . $file);
$element->name = $file;
$element->additions = 0;
$element->changes = 0;
$element->deletions = 0;
$element->additions = $numstats[$file]['additions'] ?? 0;
$element->deletions = $numstats[$file]['deletions'] ?? 0;
$element->changes = $element->additions + $element->deletions;
$element->patch = $patches[$file] ?? '';

if ($status === 'A') {
$element->status = File::STATUS_ADDED;
Expand All @@ -129,6 +142,89 @@ public function getFiles(): FileCollection
return $this->files = $files;
}

/**
* @return array<string, array{additions: int, deletions: int}>
*/
private function parseNumstat(string $output): array
{
$result = [];

foreach (explode(\PHP_EOL, $output) as $line) {
if ($line === '') {
continue;
}

$parts = preg_split('/\t/', $line, 3);
if ($parts === false || \count($parts) < 3) {
continue;
}

$result[$parts[2]] = [
'additions' => $parts[0] === '-' ? 0 : (int) $parts[0],
'deletions' => $parts[1] === '-' ? 0 : (int) $parts[1],
];
}

return $result;
}

/**
* @return array<string, string>
*/
private function parsePatch(string $output): array
{
$result = [];
$currentFile = null;
$currentPatch = '';
$fallbackFile = null;

foreach (explode(\PHP_EOL, $output) as $line) {
if (str_starts_with($line, 'diff --git ')) {
if ($currentFile !== null) {
$result[$currentFile] = $currentPatch;
}

$currentFile = null;
$currentPatch = '';
$fallbackFile = null;

continue;
}

if ($currentFile === null && str_starts_with($line, '--- a/')) {
$fallbackFile = mb_substr($line, 6);

continue;
}

if ($currentFile === null && str_starts_with($line, '+++ b/')) {
$currentFile = mb_substr($line, 6);

continue;
}

if ($currentFile === null && $line === '+++ /dev/null') {
$currentFile = $fallbackFile;

continue;
}

if ($currentFile === null && str_starts_with($line, '--- ')) {
continue;
}

if ($currentFile !== null) {
$currentPatch .= ($currentPatch !== '' ? \PHP_EOL : '') . $line;
}
}

if ($currentFile !== null) {
$result[$currentFile] = $currentPatch;
}

return $result;
}

public function getComments(): CommentCollection
{
return new CommentCollection();
Expand Down
91 changes: 91 additions & 0 deletions tests/Struct/Local/LocalPullRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,24 +98,115 @@ public function testGetFiles(): void

static::assertSame('a.txt', $fileA->name);
static::assertSame(File::STATUS_REMOVED, $fileA->status);
static::assertSame(0, $fileA->additions);
static::assertSame(1, $fileA->deletions);
static::assertSame(1, $fileA->changes);

$fileB = $files->get('b.txt');

static::assertNotNull($fileB);

static::assertSame('b2', $fileB->getContent());
static::assertSame(File::STATUS_ADDED, $fileB->status);
static::assertSame(1, $fileB->additions);
static::assertSame(0, $fileB->deletions);
static::assertSame(1, $fileB->changes);
static::assertNotEmpty($fileB->patch);

$fileC = $files->get('c.txt');

static::assertNotNull($fileC);

static::assertSame('c', $fileC->getContent());
static::assertSame(File::STATUS_ADDED, $fileC->status);
static::assertSame(1, $fileC->additions);
static::assertSame(0, $fileC->deletions);
static::assertSame(1, $fileC->changes);

$fileModified = $files->get('modified.txt');
static::assertNotNull($fileModified);
static::assertSame(File::STATUS_MODIFIED, $fileModified->status);
static::assertSame(1, $fileModified->additions);
static::assertSame(1, $fileModified->deletions);
static::assertSame(2, $fileModified->changes);
static::assertNotEmpty($fileModified->patch);
}

public function testDiffStatsForSingleAddedFile(): void
{
$pr = new LocalPullRequest($this->tmpDir, 'feature', 'main');

$files = $pr->getFiles();

static::assertCount(1, $files);

$fileB = $files->get('b.txt');
static::assertNotNull($fileB);

static::assertSame(File::STATUS_ADDED, $fileB->status);
static::assertSame(1, $fileB->additions);
static::assertSame(0, $fileB->deletions);
static::assertSame(1, $fileB->changes);
static::assertNotEmpty($fileB->patch);
}

public function testDiffStatsForMultilineChanges(): void
{
(new Process(['git', 'checkout', '-b', 'multiline'], $this->tmpDir))->mustRun();

file_put_contents($this->tmpDir . '/multi.txt', "line1\nline2\nline3\nline4\nline5\n");
(new Process(['git', 'add', 'multi.txt'], $this->tmpDir))->mustRun();
(new Process(['git', 'commit', '-m', 'add multiline file'], $this->tmpDir))->mustRun();

(new Process(['git', 'checkout', '-b', 'multiline-edit'], $this->tmpDir))->mustRun();

file_put_contents($this->tmpDir . '/multi.txt', "line1\nchanged2\nline3\nchanged4\nline5\nnewline6\n");
(new Process(['git', 'add', 'multi.txt'], $this->tmpDir))->mustRun();
(new Process(['git', 'commit', '-m', 'edit multiline file'], $this->tmpDir))->mustRun();

$pr = new LocalPullRequest($this->tmpDir, 'multiline-edit', 'multiline');

$files = $pr->getFiles();

static::assertCount(1, $files);

$file = $files->get('multi.txt');
static::assertNotNull($file);

static::assertSame(File::STATUS_MODIFIED, $file->status);
static::assertSame(3, $file->additions);
static::assertSame(2, $file->deletions);
static::assertSame(5, $file->changes);

static::assertStringContainsString('+changed2', $file->patch);
static::assertStringContainsString('-line2', $file->patch);
static::assertStringContainsString('+newline6', $file->patch);
}

public function testPatchContentForDeletedFile(): void
{
$pr = new LocalPullRequest($this->tmpDir, 'feature2', 'main');

$files = $pr->getFiles();

$fileA = $files->get('a.txt');
static::assertNotNull($fileA);
static::assertSame(File::STATUS_REMOVED, $fileA->status);
static::assertStringContainsString('-a', $fileA->patch);
}

public function testPatchContentForModifiedFile(): void
{
$pr = new LocalPullRequest($this->tmpDir, 'feature2', 'main');

$files = $pr->getFiles();

$fileModified = $files->get('modified.txt');
static::assertNotNull($fileModified);

static::assertStringContainsString('-a', $fileModified->patch);
static::assertStringContainsString('+b', $fileModified->patch);
static::assertStringContainsString('@@', $fileModified->patch);
}

public function testGetSingleFile(): void
Expand Down
Loading