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
|
|
|
|