|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* This file is part of the php-merge package. |
|
4
|
|
|
* |
|
5
|
|
|
* (c) Fabian Bircher <[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 PhpMerge; |
|
12
|
|
|
|
|
13
|
|
|
use PhpMerge\internal\Line; |
|
14
|
|
|
use PhpMerge\internal\Hunk; |
|
15
|
|
|
use PhpMerge\internal\PhpMergeBase; |
|
16
|
|
|
use SebastianBergmann\Diff\Differ; |
|
17
|
|
|
|
|
18
|
|
|
/** |
|
19
|
|
|
* Class PhpMerge merges three texts by lines. |
|
20
|
|
|
* |
|
21
|
|
|
* The merge class which in most cases will work, the diff is calculated using |
|
22
|
|
|
* an instance of \SebastianBergmann\Diff\Differ. The merge algorithm goes |
|
23
|
|
|
* through all the lines and decides which to line to use. |
|
24
|
|
|
* |
|
25
|
|
|
* @package PhpMerge |
|
26
|
|
|
* @author Fabian Bircher <[email protected]> |
|
27
|
|
|
* @copyright Fabian Bircher <[email protected]> |
|
28
|
|
|
* @license https://opensource.org/licenses/MIT |
|
29
|
|
|
* @version Release: @package_version@ |
|
30
|
|
|
* @link http://github.com/bircher/php-merge |
|
31
|
|
|
*/ |
|
32
|
|
|
final class PhpMerge extends PhpMergeBase implements PhpMergeInterface |
|
33
|
|
|
{ |
|
34
|
|
|
|
|
35
|
|
|
/** |
|
36
|
|
|
* The differ used to create the diffs. |
|
37
|
|
|
* |
|
38
|
|
|
* @var \SebastianBergmann\Diff\Differ |
|
39
|
|
|
*/ |
|
40
|
|
|
protected $differ; |
|
41
|
|
|
|
|
42
|
|
|
/** |
|
43
|
|
|
* PhpMerge constructor. |
|
44
|
|
|
*/ |
|
45
|
10 |
|
public function __construct(Differ $differ = null) |
|
46
|
|
|
{ |
|
47
|
10 |
|
if (!$differ) { |
|
48
|
10 |
|
$differ = new Differ(); |
|
49
|
|
|
} |
|
50
|
10 |
|
$this->differ = $differ; |
|
51
|
10 |
|
} |
|
52
|
|
|
|
|
53
|
|
|
|
|
54
|
|
|
/** |
|
55
|
|
|
* {@inheritdoc} |
|
56
|
|
|
*/ |
|
57
|
10 |
|
public function merge(string $base, string $remote, string $local) : string |
|
58
|
|
|
{ |
|
59
|
|
|
// Skip merging if there is nothing to do. |
|
60
|
10 |
|
if ($merged = PhpMergeBase::simpleMerge($base, $remote, $local)) { |
|
61
|
3 |
|
return $merged; |
|
62
|
|
|
} |
|
63
|
|
|
|
|
64
|
7 |
|
$remoteDiff = Line::createArray($this->differ->diffToArray($base, $remote)); |
|
65
|
7 |
|
$localDiff = Line::createArray($this->differ->diffToArray($base, $local)); |
|
66
|
|
|
|
|
67
|
7 |
|
$baseLines = Line::createArray( |
|
68
|
7 |
|
array_map( |
|
69
|
|
|
function ($l) { |
|
70
|
7 |
|
return [$l, 0]; |
|
71
|
7 |
|
}, |
|
72
|
7 |
|
self::splitStringByLines($base) |
|
73
|
|
|
) |
|
74
|
|
|
); |
|
75
|
|
|
|
|
76
|
7 |
|
$remoteHunks = Hunk::createArray($remoteDiff); |
|
77
|
7 |
|
$localHunks = Hunk::createArray($localDiff); |
|
78
|
|
|
|
|
79
|
7 |
|
$conflicts = []; |
|
80
|
7 |
|
$merged = PhpMerge::mergeHunks($baseLines, $remoteHunks, $localHunks, $conflicts); |
|
81
|
7 |
|
$merged = implode("", $merged); |
|
82
|
|
|
|
|
83
|
7 |
|
if (!empty($conflicts)) { |
|
84
|
5 |
|
throw new MergeException('A merge conflict has occurred.', $conflicts, $merged); |
|
85
|
|
|
} |
|
86
|
|
|
|
|
87
|
3 |
|
return $merged; |
|
88
|
|
|
} |
|
89
|
|
|
|
|
90
|
|
|
/** |
|
91
|
|
|
* The merge algorithm. |
|
92
|
|
|
* |
|
93
|
|
|
* @param Line[] $base |
|
94
|
|
|
* The lines of the original text. |
|
95
|
|
|
* @param Hunk[] $remote |
|
96
|
|
|
* The hunks of the remote changes. |
|
97
|
|
|
* @param Hunk[] $local |
|
98
|
|
|
* The hunks of the local changes. |
|
99
|
|
|
* @param MergeConflict[] $conflicts |
|
100
|
|
|
* The merge conflicts. |
|
101
|
|
|
* |
|
102
|
|
|
* @return string[] |
|
103
|
|
|
* The merged text. |
|
104
|
|
|
*/ |
|
105
|
7 |
|
protected static function mergeHunks(array $base, array $remote, array $local, array &$conflicts = []) : array |
|
106
|
|
|
{ |
|
107
|
7 |
|
$remote = new \ArrayObject($remote); |
|
108
|
7 |
|
$local = new \ArrayObject($local); |
|
109
|
|
|
|
|
110
|
7 |
|
$merged = []; |
|
111
|
|
|
|
|
112
|
7 |
|
$a = $remote->getIterator(); |
|
113
|
7 |
|
$b = $local->getIterator(); |
|
114
|
7 |
|
$flipped = false; |
|
115
|
7 |
|
$i = -1; |
|
116
|
|
|
|
|
117
|
|
|
// Loop over all indexes of the base and all hunks. |
|
118
|
7 |
|
while ($i < count($base) || $a->valid() || $b->valid()) { |
|
119
|
|
|
// Assure that $aa is the first hunk by swaping $a and $b |
|
120
|
7 |
|
if ($a->valid() && $b->valid() && $a->current()->getStart() > $b->current()->getStart()) { |
|
121
|
5 |
|
self::swap($a, $b, $flipped); |
|
122
|
7 |
|
} elseif (!$a->valid() && $b->valid()) { |
|
123
|
3 |
|
self::swap($a, $b, $flipped); |
|
124
|
|
|
} |
|
125
|
|
|
/** @var Hunk $aa */ |
|
126
|
7 |
|
$aa = $a->current(); |
|
127
|
|
|
/** @var Hunk $bb */ |
|
128
|
7 |
|
$bb = $b->current(); |
|
129
|
|
|
|
|
130
|
7 |
|
if ($aa) { |
|
131
|
7 |
|
assert($aa->getStart() >= $i, 'The start of the hunk is after the current index.'); |
|
132
|
|
|
} |
|
133
|
|
|
// The hunk starts at the current index. |
|
134
|
7 |
|
if ($aa && $aa->getStart() == $i) { |
|
135
|
|
|
// Hunks from both sources start with the same index. |
|
136
|
7 |
|
if ($bb && $bb->getStart() == $i) { |
|
137
|
6 |
|
if ($aa != $bb) { |
|
138
|
|
|
// If the hunks are not the same its a conflict. |
|
139
|
5 |
|
$conflicts[] = self::prepareConflict($base, $a, $b, $flipped, count($merged)); |
|
140
|
5 |
|
$aa = $a->current(); |
|
141
|
|
|
} else { |
|
142
|
|
|
// Advance $b it is the same as $a and will be merged. |
|
143
|
6 |
|
$b->next(); |
|
144
|
|
|
} |
|
145
|
5 |
|
} elseif ($aa->hasIntersection($bb)) { |
|
146
|
|
|
// The end overlaps with the start of the next other hunk. |
|
147
|
2 |
|
$conflicts[] = self::prepareConflict($base, $a, $b, $flipped, count($merged)); |
|
148
|
2 |
|
$aa = $a->current(); |
|
149
|
|
|
} |
|
150
|
|
|
} |
|
151
|
|
|
// The conflict resolution could mean the hunk starts now later. |
|
152
|
7 |
|
if ($aa && $aa->getStart() == $i) { |
|
153
|
7 |
|
if ($aa->getType() == Hunk::ADDED && $i >= 0) { |
|
154
|
5 |
|
$merged[] = $base[$i]->getContent(); |
|
155
|
|
|
} |
|
156
|
|
|
|
|
157
|
7 |
|
if ($aa->getType() != Hunk::REMOVED) { |
|
158
|
7 |
|
foreach ($aa->getAddedLines() as $line) { |
|
159
|
7 |
|
$merged[] = $line->getContent(); |
|
160
|
|
|
} |
|
161
|
|
|
} |
|
162
|
7 |
|
$i = $aa->getEnd(); |
|
163
|
7 |
|
$a->next(); |
|
164
|
|
|
} else { |
|
165
|
|
|
// Not dealing with a change, so return the line from the base. |
|
166
|
7 |
|
if ($i >= 0) { |
|
167
|
7 |
|
$merged[] = $base[$i]->getContent(); |
|
168
|
|
|
} |
|
169
|
|
|
} |
|
170
|
|
|
// Finally, advance the index. |
|
171
|
7 |
|
$i++; |
|
172
|
|
|
} |
|
173
|
7 |
|
return $merged; |
|
174
|
|
|
} |
|
175
|
|
|
|
|
176
|
|
|
/** |
|
177
|
|
|
* Get a Merge conflict from the two array iterators. |
|
178
|
|
|
* |
|
179
|
|
|
* @param Line[] $base |
|
180
|
|
|
* The original lines of the base text. |
|
181
|
|
|
* @param \ArrayIterator $a |
|
182
|
|
|
* The first hunk iterator. |
|
183
|
|
|
* @param \ArrayIterator $b |
|
184
|
|
|
* The second hunk iterator. |
|
185
|
|
|
* @param bool $flipped |
|
186
|
|
|
* Whether or not the a corresponds to remote and b to local. |
|
187
|
|
|
* @param int $mergedLine |
|
188
|
|
|
* The line on which the merge conflict appears on the merged result. |
|
189
|
|
|
* |
|
190
|
|
|
* @return MergeConflict |
|
191
|
|
|
* The merge conflict. |
|
192
|
|
|
*/ |
|
193
|
5 |
|
protected static function prepareConflict($base, &$a, &$b, &$flipped, $mergedLine) |
|
194
|
|
|
{ |
|
195
|
5 |
|
if ($flipped) { |
|
196
|
2 |
|
self::swap($a, $b, $flipped); |
|
197
|
|
|
} |
|
198
|
|
|
/** @var Hunk $aa */ |
|
199
|
5 |
|
$aa = $a->current(); |
|
200
|
|
|
/** @var Hunk $bb */ |
|
201
|
5 |
|
$bb = $b->current(); |
|
202
|
|
|
|
|
203
|
|
|
// If one of the hunks is added but the other one does not start there. |
|
204
|
5 |
|
if ($aa->getType() == Hunk::ADDED && $bb->getType() != Hunk::ADDED) { |
|
205
|
3 |
|
$start = $bb->getStart(); |
|
206
|
3 |
|
$end = $bb->getEnd(); |
|
207
|
5 |
|
} elseif ($aa->getType() != Hunk::ADDED && $bb->getType() == Hunk::ADDED) { |
|
208
|
3 |
|
$start = $aa->getStart(); |
|
209
|
3 |
|
$end = $aa->getEnd(); |
|
210
|
|
|
} else { |
|
211
|
4 |
|
$start = min($aa->getStart(), $bb->getStart()); |
|
212
|
4 |
|
$end = max($aa->getEnd(), $bb->getEnd()); |
|
213
|
|
|
} |
|
214
|
|
|
// Add one to the merged line number if we advanced the start. |
|
215
|
5 |
|
$mergedLine += $start - min($aa->getStart(), $bb->getStart()); |
|
216
|
|
|
|
|
217
|
5 |
|
$baseLines = []; |
|
218
|
5 |
|
$remoteLines = []; |
|
219
|
5 |
|
$localLines = []; |
|
220
|
5 |
|
if ($aa->getType() != Hunk::ADDED || $bb->getType() != Hunk::ADDED) { |
|
221
|
|
|
// If the start is after the start of the hunk, include it first. |
|
222
|
5 |
|
if ($aa->getStart() < $start) { |
|
223
|
1 |
|
$remoteLines = $aa->getLinesContent(); |
|
224
|
|
|
} |
|
225
|
5 |
|
if ($bb->getStart() < $start) { |
|
226
|
1 |
|
$localLines = $bb->getLinesContent(); |
|
227
|
|
|
} |
|
228
|
5 |
|
for ($i = $start; $i <= $end; $i++) { |
|
229
|
5 |
|
$baseLines[] = $base[$i]->getContent(); |
|
230
|
|
|
// For conflicts that happened on overlapping lines. |
|
231
|
5 |
|
if ($i < $aa->getStart() || $i > $aa->getEnd()) { |
|
232
|
2 |
|
$remoteLines[] = $base[$i]->getContent(); |
|
233
|
5 |
|
} elseif ($i == $aa->getStart()) { |
|
234
|
5 |
|
if ($aa->getType() == Hunk::ADDED) { |
|
235
|
2 |
|
$remoteLines[] = $base[$i]->getContent(); |
|
236
|
|
|
} |
|
237
|
5 |
|
$remoteLines = array_merge($remoteLines, $aa->getLinesContent()); |
|
238
|
|
|
} |
|
239
|
5 |
|
if ($i < $bb->getStart() || $i > $bb->getEnd()) { |
|
240
|
2 |
|
$localLines[] = $base[$i]->getContent(); |
|
241
|
5 |
|
} elseif ($i == $bb->getStart()) { |
|
242
|
5 |
|
if ($bb->getType() == Hunk::ADDED) { |
|
243
|
2 |
|
$localLines[] = $base[$i]->getContent(); |
|
244
|
|
|
} |
|
245
|
5 |
|
$localLines = array_merge($localLines, $bb->getLinesContent()); |
|
246
|
|
|
} |
|
247
|
|
|
} |
|
248
|
|
|
} else { |
|
249
|
2 |
|
$remoteLines = $aa->getLinesContent(); |
|
250
|
2 |
|
$localLines = $bb->getLinesContent(); |
|
251
|
|
|
} |
|
252
|
|
|
|
|
253
|
5 |
|
$b->next(); |
|
254
|
5 |
|
return new MergeConflict($baseLines, $remoteLines, $localLines, $start, $mergedLine); |
|
255
|
|
|
} |
|
256
|
|
|
|
|
257
|
|
|
/** |
|
258
|
|
|
* Swaps two variables. |
|
259
|
|
|
* |
|
260
|
|
|
* @param mixed $a |
|
261
|
|
|
* The first variable which will become the second. |
|
262
|
|
|
* @param mixed $b |
|
263
|
|
|
* The second variable which will become the first. |
|
264
|
|
|
* @param bool $flipped |
|
265
|
|
|
* The boolean indicator which will change its value. |
|
266
|
|
|
*/ |
|
267
|
5 |
|
protected static function swap(&$a, &$b, &$flipped) |
|
268
|
|
|
{ |
|
269
|
5 |
|
$c = $a; |
|
270
|
5 |
|
$a = $b; |
|
271
|
5 |
|
$b = $c; |
|
272
|
5 |
|
$flipped = !$flipped; |
|
273
|
5 |
|
} |
|
274
|
|
|
} |
|
275
|
|
|
|