Skip to content

Commit 00d8110

Browse files
SpacePossumsebastianbergmann
authored andcommitted
- Add context lines
- Add strict udiff output format
1 parent 0fd4d14 commit 00d8110

25 files changed

+3231
-496
lines changed

TODO.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Docs

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "sebastian/diff",
33
"description": "Diff implementation",
4-
"keywords": ["diff"],
4+
"keywords": ["diff", "udiff", "unidiff", "unified diff"],
55
"homepage": "https://github.com/sebastianbergmann/diff",
66
"license": "BSD-3-Clause",
77
"authors": [
@@ -18,7 +18,8 @@
1818
"php": "^7.0"
1919
},
2020
"require-dev": {
21-
"phpunit/phpunit": "^6.2"
21+
"phpunit/phpunit": "^6.2",
22+
"symfony/process": "^3.3"
2223
},
2324
"autoload": {
2425
"classmap": [

src/Differ.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
namespace SebastianBergmann\Diff;
1212

13-
use SebastianBergmann\Diff\InvalidArgumentException;
1413
use SebastianBergmann\Diff\Output\DiffOutputBuilderInterface;
1514
use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder;
1615

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of sebastian/diff.
4+
*
5+
* (c) Sebastian Bergmann <[email protected]>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
11+
namespace SebastianBergmann\Diff;
12+
13+
final class ConfigurationException extends InvalidArgumentException
14+
{
15+
/**
16+
* @param string $option
17+
* @param string $expected
18+
* @param mixed $value
19+
* @param int $code
20+
* @param Exception|null $previous
21+
*/
22+
public function __construct(
23+
string $option,
24+
string $expected,
25+
$value,
26+
int $code = 0,
27+
Exception $previous = null
28+
) {
29+
parent::__construct(
30+
\sprintf(
31+
'Option "%s" must be %s, got "%s".',
32+
$option,
33+
$expected,
34+
\is_object($value) ? \get_class($value) : (null === $value ? '<null>' : \gettype($value) . '#' . $value)
35+
),
36+
$code,
37+
$previous
38+
);
39+
}
40+
}
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of sebastian/diff.
4+
*
5+
* (c) Sebastian Bergmann <[email protected]>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
11+
namespace SebastianBergmann\Diff\Output;
12+
13+
use SebastianBergmann\Diff\ConfigurationException;
14+
15+
/**
16+
* Strict Unified diff output builder.
17+
*
18+
* @name Unified diff output builder
19+
*
20+
* @description Generates (strict) Unified diff's (unidiffs) with hunks.
21+
*
22+
* @author SpacePossum
23+
*
24+
* @api
25+
*/
26+
final class StrictUnifiedDiffOutputBuilder implements DiffOutputBuilderInterface
27+
{
28+
/**
29+
* @var int
30+
*/
31+
private static $noNewlineAtOEFid = 998877;
32+
33+
/**
34+
* @var bool
35+
*/
36+
private $changed;
37+
38+
/**
39+
* @var bool
40+
*/
41+
private $collapseRanges;
42+
43+
/**
44+
* @var int >= 0
45+
*/
46+
private $commonLineThreshold;
47+
48+
/**
49+
* @var string
50+
*/
51+
private $header;
52+
53+
/**
54+
* @var int >= 0
55+
*/
56+
private $contextLines;
57+
58+
private static $default = [
59+
'collapseRanges' => true, // ranges of length one are rendered with the trailing `,1`
60+
'commonLineThreshold' => 6, // number of same lines before ending a new hunk and creating a new one (if needed)
61+
'contextLines' => 3, // like `diff: -u, -U NUM, --unified[=NUM]`, for patch/git apply compatibility best to keep at least @ 3
62+
'fromFile' => null,
63+
'fromFileDate' => null,
64+
'toFile' => null,
65+
'toFileDate' => null,
66+
];
67+
68+
public function __construct(array $options = [])
69+
{
70+
$options = \array_merge(self::$default, $options);
71+
72+
if (!\is_bool($options['collapseRanges'])) {
73+
throw new ConfigurationException('collapseRanges', 'a bool', $options['collapseRanges']);
74+
}
75+
76+
if (!\is_int($options['contextLines']) || $options['contextLines'] < 0) {
77+
throw new ConfigurationException('contextLines', 'an int >= 0', $options['contextLines']);
78+
}
79+
80+
if (!\is_int($options['commonLineThreshold']) || $options['commonLineThreshold'] < 1) {
81+
throw new ConfigurationException('commonLineThreshold', 'an int > 0', $options['commonLineThreshold']);
82+
}
83+
84+
foreach (['fromFile', 'toFile'] as $option) {
85+
if (!\is_string($options[$option])) {
86+
throw new ConfigurationException($option, 'a string', $options[$option]);
87+
}
88+
}
89+
90+
foreach (['fromFileDate', 'toFileDate'] as $option) {
91+
if (null !== $options[$option] && !\is_string($options[$option])) {
92+
throw new ConfigurationException($option, 'a string or <null>', $options[$option]);
93+
}
94+
}
95+
96+
$this->header = \sprintf(
97+
"--- %s%s\n+++ %s%s\n",
98+
$options['fromFile'],
99+
null === $options['fromFileDate'] ? '' : "\t" . $options['fromFileDate'],
100+
$options['toFile'],
101+
null === $options['toFileDate'] ? '' : "\t" . $options['toFileDate']
102+
);
103+
104+
$this->collapseRanges = $options['collapseRanges'];
105+
$this->commonLineThreshold = $options['commonLineThreshold'];
106+
$this->contextLines = $options['contextLines'];
107+
}
108+
109+
public function getDiff(array $diff): string
110+
{
111+
if (0 === \count($diff)) {
112+
return '';
113+
}
114+
115+
$this->changed = false;
116+
117+
$buffer = \fopen('php://memory', 'r+b');
118+
\fwrite($buffer, $this->header);
119+
120+
$this->writeDiffHunks($buffer, $diff);
121+
122+
if (!$this->changed) {
123+
\fclose($buffer);
124+
125+
return '';
126+
}
127+
128+
$diff = \stream_get_contents($buffer, -1, 0);
129+
130+
\fclose($buffer);
131+
132+
// If the last char is not a linebreak: add it.
133+
// This might happen when both the `from` and `to` do not have a trailing linebreak
134+
$last = \substr($diff, -1);
135+
136+
return "\n" !== $last && "\r" !== $last
137+
? $diff . "\n"
138+
: $diff
139+
;
140+
}
141+
142+
private function writeDiffHunks($output, array $diff)
143+
{
144+
// detect "No newline at end of file" and insert into `$diff` if needed
145+
146+
$upperLimit = \count($diff);
147+
148+
if (0 === $diff[$upperLimit - 1][1]) {
149+
$lc = \substr($diff[$upperLimit - 1][0], -1);
150+
if ("\n" !== $lc) {
151+
\array_splice($diff, $upperLimit, 0, [["\n\\ No newline at end of file\n", self::$noNewlineAtOEFid]]);
152+
}
153+
} else {
154+
// search back for the last `+` and `-` line,
155+
// check if has trailing linebreak, else add under it warning under it
156+
$toFind = [1 => true, 2 => true];
157+
for ($i = $upperLimit - 1; $i >= 0; --$i) {
158+
if (isset($toFind[$diff[$i][1]])) {
159+
unset($toFind[$diff[$i][1]]);
160+
$lc = \substr($diff[$i][0], -1);
161+
if ("\n" !== $lc) {
162+
\array_splice($diff, $i + 1, 0, [["\n\\ No newline at end of file\n", self::$noNewlineAtOEFid]]);
163+
}
164+
165+
if (!\count($toFind)) {
166+
break;
167+
}
168+
}
169+
}
170+
}
171+
172+
// write hunks to output buffer
173+
174+
$cutOff = \max($this->commonLineThreshold, $this->contextLines);
175+
$hunkCapture = false;
176+
$sameCount = $toRange = $fromRange = 0;
177+
$toStart = $fromStart = 1;
178+
179+
foreach ($diff as $i => $entry) {
180+
if (0 === $entry[1]) { // same
181+
if (false === $hunkCapture) {
182+
++$fromStart;
183+
++$toStart;
184+
185+
continue;
186+
}
187+
188+
++$sameCount;
189+
++$toRange;
190+
++$fromRange;
191+
192+
if ($sameCount === $cutOff) {
193+
$contextStartOffset = ($hunkCapture - $this->contextLines) < 0
194+
? $hunkCapture
195+
: $this->contextLines
196+
;
197+
198+
// note: $contextEndOffset = $this->contextLines;
199+
//
200+
// because we never go beyond the end of the diff.
201+
// with the cutoff/contextlines here the follow is never true;
202+
//
203+
// if ($i - $cutOff + $this->contextLines + 1 > \count($diff)) {
204+
// $contextEndOffset = count($diff) - 1;
205+
// }
206+
//
207+
// ; that would be true for a trailing incomplete hunk case which is dealt with after this loop
208+
209+
$this->writeHunk(
210+
$diff,
211+
$hunkCapture - $contextStartOffset,
212+
$i - $cutOff + $this->contextLines + 1,
213+
$fromStart - $contextStartOffset,
214+
$fromRange - $cutOff + $contextStartOffset + $this->contextLines,
215+
$toStart - $contextStartOffset,
216+
$toRange - $cutOff + $contextStartOffset + $this->contextLines,
217+
$output
218+
);
219+
220+
$fromStart += $fromRange;
221+
$toStart += $toRange;
222+
223+
$hunkCapture = false;
224+
$sameCount = $toRange = $fromRange = 0;
225+
}
226+
227+
continue;
228+
}
229+
230+
$sameCount = 0;
231+
232+
if ($entry[1] === self::$noNewlineAtOEFid) {
233+
continue;
234+
}
235+
236+
$this->changed = true;
237+
238+
if (false === $hunkCapture) {
239+
$hunkCapture = $i;
240+
}
241+
242+
if (1 === $entry[1]) { // added
243+
++$toRange;
244+
}
245+
246+
if (2 === $entry[1]) { // removed
247+
++$fromRange;
248+
}
249+
}
250+
251+
if (false === $hunkCapture) {
252+
return;
253+
}
254+
255+
// we end here when cutoff (commonLineThreshold) was not reached, but we where capturing a hunk,
256+
// do not render hunk till end automatically because the number of context lines might be less than the commonLineThreshold
257+
258+
$contextStartOffset = $hunkCapture - $this->contextLines < 0
259+
? $hunkCapture
260+
: $this->contextLines
261+
;
262+
263+
// prevent trying to write out more common lines than there are in the diff _and_
264+
// do not write more than configured through the context lines
265+
$contextEndOffset = \min($sameCount, $this->contextLines);
266+
267+
$fromRange -= $sameCount;
268+
$toRange -= $sameCount;
269+
270+
$this->writeHunk(
271+
$diff,
272+
$hunkCapture - $contextStartOffset,
273+
$i - $sameCount + $contextEndOffset + 1,
274+
$fromStart - $contextStartOffset,
275+
$fromRange + $contextStartOffset + $contextEndOffset,
276+
$toStart - $contextStartOffset,
277+
$toRange + $contextStartOffset + $contextEndOffset,
278+
$output
279+
);
280+
}
281+
282+
private function writeHunk(
283+
array $diff,
284+
int $diffStartIndex,
285+
int $diffEndIndex,
286+
int $fromStart,
287+
int $fromRange,
288+
int $toStart,
289+
int $toRange,
290+
$output
291+
) {
292+
\fwrite($output, '@@ -' . $fromStart);
293+
294+
if (!$this->collapseRanges || 1 !== $fromRange) {
295+
\fwrite($output, ',' . $fromRange);
296+
}
297+
298+
\fwrite($output, ' +' . $toStart);
299+
if (!$this->collapseRanges || 1 !== $toRange) {
300+
\fwrite($output, ',' . $toRange);
301+
}
302+
303+
\fwrite($output, " @@\n");
304+
305+
for ($i = $diffStartIndex; $i < $diffEndIndex; ++$i) {
306+
if ($diff[$i][1] === 1) { // added
307+
$this->changed = true;
308+
\fwrite($output, '+' . $diff[$i][0]);
309+
} elseif ($diff[$i][1] === 2) { // removed
310+
$this->changed = true;
311+
\fwrite($output, '-' . $diff[$i][0]);
312+
} elseif ($diff[$i][1] === 0) { // same
313+
\fwrite($output, ' ' . $diff[$i][0]);
314+
} elseif ($diff[$i][1] === self::$noNewlineAtOEFid) {
315+
$this->changed = true;
316+
\fwrite($output, $diff[$i][0]);
317+
}
318+
//} elseif ($diff[$i][1] === 3) { // custom comment inserted by PHPUnit/diff package
319+
// skip
320+
//} else {
321+
// unknown/invalid
322+
//}
323+
}
324+
}
325+
}

0 commit comments

Comments
 (0)