Skip to content

Commit 8f220a0

Browse files
authored
Merge pull request #902 from shyim/feat/local-pull-request-diff-stats
feat: add diff stats and patch content for local pull requests
2 parents e9771b4 + 4cfe23a commit 8f220a0

2 files changed

Lines changed: 197 additions & 10 deletions

File tree

src/Struct/Local/LocalPullRequest.php

Lines changed: 106 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,30 @@ public function getFiles(): FileCollection
9090
return $this->files;
9191
}
9292

93-
$process = new Process([
94-
'git',
95-
'diff',
96-
$this->target . '..' . $this->local,
97-
'--name-status',
93+
$diffRange = $this->target . '..' . $this->local;
94+
95+
$nameStatusProcess = new Process([
96+
'git', 'diff', $diffRange, '--name-status',
9897
], $this->repo);
9998

100-
$process->mustRun();
99+
$numstatProcess = new Process([
100+
'git', 'diff', $diffRange, '--numstat',
101+
], $this->repo);
102+
103+
$patchProcess = new Process([
104+
'git', 'diff', $diffRange,
105+
], $this->repo);
106+
107+
$nameStatusProcess->mustRun();
108+
$numstatProcess->mustRun();
109+
$patchProcess->mustRun();
110+
111+
$numstats = $this->parseNumstat($numstatProcess->getOutput());
112+
$patches = $this->parsePatch($patchProcess->getOutput());
101113

102114
$files = new FileCollection();
103115

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

112124
$element = new LocalFile($this->repo . '/' . $file);
113125
$element->name = $file;
114-
$element->additions = 0;
115-
$element->changes = 0;
116-
$element->deletions = 0;
126+
$element->additions = $numstats[$file]['additions'] ?? 0;
127+
$element->deletions = $numstats[$file]['deletions'] ?? 0;
128+
$element->changes = $element->additions + $element->deletions;
129+
$element->patch = $patches[$file] ?? '';
117130

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

145+
/**
146+
* @return array<string, array{additions: int, deletions: int}>
147+
*/
148+
private function parseNumstat(string $output): array
149+
{
150+
$result = [];
151+
152+
foreach (explode(\PHP_EOL, $output) as $line) {
153+
if ($line === '') {
154+
continue;
155+
}
156+
157+
$parts = preg_split('/\t/', $line, 3);
158+
if ($parts === false || \count($parts) < 3) {
159+
continue;
160+
}
161+
162+
$result[$parts[2]] = [
163+
'additions' => $parts[0] === '-' ? 0 : (int) $parts[0],
164+
'deletions' => $parts[1] === '-' ? 0 : (int) $parts[1],
165+
];
166+
}
167+
168+
return $result;
169+
}
170+
171+
/**
172+
* @return array<string, string>
173+
*/
174+
private function parsePatch(string $output): array
175+
{
176+
$result = [];
177+
$currentFile = null;
178+
$currentPatch = '';
179+
$fallbackFile = null;
180+
181+
foreach (explode(\PHP_EOL, $output) as $line) {
182+
if (str_starts_with($line, 'diff --git ')) {
183+
if ($currentFile !== null) {
184+
$result[$currentFile] = $currentPatch;
185+
}
186+
187+
$currentFile = null;
188+
$currentPatch = '';
189+
$fallbackFile = null;
190+
191+
continue;
192+
}
193+
194+
if ($currentFile === null && str_starts_with($line, '--- a/')) {
195+
$fallbackFile = mb_substr($line, 6);
196+
197+
continue;
198+
}
199+
200+
if ($currentFile === null && str_starts_with($line, '+++ b/')) {
201+
$currentFile = mb_substr($line, 6);
202+
203+
continue;
204+
}
205+
206+
if ($currentFile === null && $line === '+++ /dev/null') {
207+
$currentFile = $fallbackFile;
208+
209+
continue;
210+
}
211+
212+
if ($currentFile === null && str_starts_with($line, '--- ')) {
213+
continue;
214+
}
215+
216+
if ($currentFile !== null) {
217+
$currentPatch .= ($currentPatch !== '' ? \PHP_EOL : '') . $line;
218+
}
219+
}
220+
221+
if ($currentFile !== null) {
222+
$result[$currentFile] = $currentPatch;
223+
}
224+
225+
return $result;
226+
}
227+
132228
public function getComments(): CommentCollection
133229
{
134230
return new CommentCollection();

tests/Struct/Local/LocalPullRequestTest.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,24 +98,115 @@ public function testGetFiles(): void
9898

9999
static::assertSame('a.txt', $fileA->name);
100100
static::assertSame(File::STATUS_REMOVED, $fileA->status);
101+
static::assertSame(0, $fileA->additions);
102+
static::assertSame(1, $fileA->deletions);
103+
static::assertSame(1, $fileA->changes);
101104

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

104107
static::assertNotNull($fileB);
105108

106109
static::assertSame('b2', $fileB->getContent());
107110
static::assertSame(File::STATUS_ADDED, $fileB->status);
111+
static::assertSame(1, $fileB->additions);
112+
static::assertSame(0, $fileB->deletions);
113+
static::assertSame(1, $fileB->changes);
114+
static::assertNotEmpty($fileB->patch);
108115

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

111118
static::assertNotNull($fileC);
112119

113120
static::assertSame('c', $fileC->getContent());
114121
static::assertSame(File::STATUS_ADDED, $fileC->status);
122+
static::assertSame(1, $fileC->additions);
123+
static::assertSame(0, $fileC->deletions);
124+
static::assertSame(1, $fileC->changes);
115125

116126
$fileModified = $files->get('modified.txt');
117127
static::assertNotNull($fileModified);
118128
static::assertSame(File::STATUS_MODIFIED, $fileModified->status);
129+
static::assertSame(1, $fileModified->additions);
130+
static::assertSame(1, $fileModified->deletions);
131+
static::assertSame(2, $fileModified->changes);
132+
static::assertNotEmpty($fileModified->patch);
133+
}
134+
135+
public function testDiffStatsForSingleAddedFile(): void
136+
{
137+
$pr = new LocalPullRequest($this->tmpDir, 'feature', 'main');
138+
139+
$files = $pr->getFiles();
140+
141+
static::assertCount(1, $files);
142+
143+
$fileB = $files->get('b.txt');
144+
static::assertNotNull($fileB);
145+
146+
static::assertSame(File::STATUS_ADDED, $fileB->status);
147+
static::assertSame(1, $fileB->additions);
148+
static::assertSame(0, $fileB->deletions);
149+
static::assertSame(1, $fileB->changes);
150+
static::assertNotEmpty($fileB->patch);
151+
}
152+
153+
public function testDiffStatsForMultilineChanges(): void
154+
{
155+
(new Process(['git', 'checkout', '-b', 'multiline'], $this->tmpDir))->mustRun();
156+
157+
file_put_contents($this->tmpDir . '/multi.txt', "line1\nline2\nline3\nline4\nline5\n");
158+
(new Process(['git', 'add', 'multi.txt'], $this->tmpDir))->mustRun();
159+
(new Process(['git', 'commit', '-m', 'add multiline file'], $this->tmpDir))->mustRun();
160+
161+
(new Process(['git', 'checkout', '-b', 'multiline-edit'], $this->tmpDir))->mustRun();
162+
163+
file_put_contents($this->tmpDir . '/multi.txt', "line1\nchanged2\nline3\nchanged4\nline5\nnewline6\n");
164+
(new Process(['git', 'add', 'multi.txt'], $this->tmpDir))->mustRun();
165+
(new Process(['git', 'commit', '-m', 'edit multiline file'], $this->tmpDir))->mustRun();
166+
167+
$pr = new LocalPullRequest($this->tmpDir, 'multiline-edit', 'multiline');
168+
169+
$files = $pr->getFiles();
170+
171+
static::assertCount(1, $files);
172+
173+
$file = $files->get('multi.txt');
174+
static::assertNotNull($file);
175+
176+
static::assertSame(File::STATUS_MODIFIED, $file->status);
177+
static::assertSame(3, $file->additions);
178+
static::assertSame(2, $file->deletions);
179+
static::assertSame(5, $file->changes);
180+
181+
static::assertStringContainsString('+changed2', $file->patch);
182+
static::assertStringContainsString('-line2', $file->patch);
183+
static::assertStringContainsString('+newline6', $file->patch);
184+
}
185+
186+
public function testPatchContentForDeletedFile(): void
187+
{
188+
$pr = new LocalPullRequest($this->tmpDir, 'feature2', 'main');
189+
190+
$files = $pr->getFiles();
191+
192+
$fileA = $files->get('a.txt');
193+
static::assertNotNull($fileA);
194+
static::assertSame(File::STATUS_REMOVED, $fileA->status);
195+
static::assertStringContainsString('-a', $fileA->patch);
196+
}
197+
198+
public function testPatchContentForModifiedFile(): void
199+
{
200+
$pr = new LocalPullRequest($this->tmpDir, 'feature2', 'main');
201+
202+
$files = $pr->getFiles();
203+
204+
$fileModified = $files->get('modified.txt');
205+
static::assertNotNull($fileModified);
206+
207+
static::assertStringContainsString('-a', $fileModified->patch);
208+
static::assertStringContainsString('+b', $fileModified->patch);
209+
static::assertStringContainsString('@@', $fileModified->patch);
119210
}
120211

121212
public function testGetSingleFile(): void

0 commit comments

Comments
 (0)