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 GitWrapper\GitWrapper; |
14
|
|
|
use GitWrapper\GitException; |
15
|
|
|
use PhpMerge\internal\Line; |
16
|
|
|
use PhpMerge\internal\Hunk; |
17
|
|
|
use PhpMerge\internal\PhpMergeBase; |
18
|
|
|
use SebastianBergmann\Diff\Differ; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* Class GitMerge merges three strings with git as the backend. |
22
|
|
|
* |
23
|
|
|
* A temporary directory is created and a git repository is initialised in it, |
24
|
|
|
* then a file is created within the directory containing the string to merge. |
25
|
|
|
* This was the original merge class but while it is nice not to have to deal |
26
|
|
|
* with merging, it has a considerable performance implication. So now this |
27
|
|
|
* implementation serves as a reference to make sure the other classes behave. |
28
|
|
|
* |
29
|
|
|
* @package PhpMerge |
30
|
|
|
* @author Fabian Bircher <[email protected]> |
31
|
|
|
* @copyright 2015 Fabian Bircher <[email protected]> |
32
|
|
|
* @license https://opensource.org/licenses/MIT |
33
|
|
|
* @version Release: @package_version@ |
34
|
|
|
* @link http://github.com/bircher/php-merge |
35
|
|
|
* @category library |
36
|
|
|
*/ |
37
|
|
|
final class GitMerge extends PhpMergeBase implements PhpMergeInterface |
38
|
|
|
{ |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* The git working directory. |
42
|
|
|
* |
43
|
|
|
* @var \GitWrapper\GitWorkingCopy |
44
|
|
|
*/ |
45
|
|
|
protected $git; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* The git wrapper to use for merging. |
49
|
|
|
* |
50
|
|
|
* @var \GitWrapper\GitWrapper |
51
|
|
|
*/ |
52
|
|
|
protected $wrapper; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* The temporary directory in which git can work. |
56
|
|
|
* @var string |
57
|
|
|
*/ |
58
|
|
|
protected $dir; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* The text of the last conflict |
62
|
|
|
* @var string |
63
|
|
|
*/ |
64
|
|
|
protected $conflict; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* {@inheritdoc} |
68
|
|
|
*/ |
69
|
11 |
|
public function merge(string $base, string $remote, string $local) : string |
70
|
|
|
{ |
71
|
|
|
|
72
|
|
|
// Skip merging if there is nothing to do. |
73
|
11 |
|
if ($merged = PhpMergeBase::simpleMerge($base, $remote, $local)) { |
74
|
3 |
|
return $merged; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
// Only set up the git wrapper if we really merge something. |
78
|
8 |
|
$this->setup(); |
79
|
|
|
|
80
|
8 |
|
$file = tempnam($this->dir, ''); |
81
|
|
|
try { |
82
|
8 |
|
return $this->mergeFile($file, $base, $remote, $local); |
83
|
5 |
|
} catch (GitException $e) { |
|
|
|
|
84
|
|
|
// Get conflicts by reading from the file. |
85
|
5 |
|
$conflicts = []; |
86
|
5 |
|
$merged = []; |
87
|
5 |
|
self::getConflicts($file, $base, $remote, $local, $conflicts, $merged); |
88
|
5 |
|
$merged = implode("", $merged); |
89
|
|
|
// Set the file to the merged one with the first text for conflicts. |
90
|
5 |
|
file_put_contents($file, $merged); |
91
|
5 |
|
$this->git->add($file); |
92
|
5 |
|
$this->git->commit('Resolve merge conflict.'); |
93
|
5 |
|
throw new MergeException('A merge conflict has occurred.', $conflicts, $merged, 0, $e); |
94
|
|
|
} |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Merge three strings in a specified file. |
99
|
|
|
* |
100
|
|
|
* @param string $file |
101
|
|
|
* The file name in the git repository to which the content is written. |
102
|
|
|
* @param string $base |
103
|
|
|
* The common base text. |
104
|
|
|
* @param string $remote |
105
|
|
|
* The first changed text. |
106
|
|
|
* @param string $local |
107
|
|
|
* The second changed text |
108
|
|
|
* |
109
|
|
|
* @return string |
110
|
|
|
* The merged text. |
111
|
|
|
*/ |
112
|
8 |
|
protected function mergeFile(string $file, string $base, string $remote, string $local) : string |
113
|
|
|
{ |
114
|
8 |
|
file_put_contents($file, $base); |
115
|
8 |
|
$this->git->add($file); |
116
|
8 |
|
$this->git->commit('Add base.'); |
117
|
|
|
|
118
|
8 |
|
if (!in_array('original', $this->git->getBranches()->all())) { |
119
|
8 |
|
$this->git->checkoutNewBranch('original'); |
120
|
|
|
} else { |
121
|
7 |
|
$this->git->checkout('original'); |
122
|
7 |
|
$this->git->rebase('master'); |
123
|
|
|
} |
124
|
|
|
|
125
|
8 |
|
file_put_contents($file, $remote); |
126
|
8 |
|
$this->git->add($file); |
127
|
8 |
|
$this->git->commit('Add remote.'); |
128
|
|
|
|
129
|
8 |
|
$this->git->checkout('master'); |
130
|
|
|
|
131
|
8 |
|
file_put_contents($file, $local); |
132
|
8 |
|
$this->git->add($file); |
133
|
8 |
|
$this->git->commit('Add local.'); |
134
|
|
|
|
135
|
8 |
|
$this->git->merge('original'); |
136
|
4 |
|
return file_get_contents($file); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Get the conflicts from a file which is left with merge conflicts. |
141
|
|
|
* |
142
|
|
|
* @param string $file |
143
|
|
|
* The file name. |
144
|
|
|
* @param string $baseText |
145
|
|
|
* The original text used for merging. |
146
|
|
|
* @param string $remoteText |
147
|
|
|
* The first chaned text. |
148
|
|
|
* @param string $localText |
149
|
|
|
* The second changed text. |
150
|
|
|
* @param MergeConflict[] $conflicts |
151
|
|
|
* The merge conflicts will be apended to this array. |
152
|
|
|
* @param string[] $merged |
153
|
|
|
* The merged text resolving conflicts by using the first set of changes. |
154
|
|
|
*/ |
155
|
5 |
|
protected static function getConflicts($file, $baseText, $remoteText, $localText, &$conflicts, &$merged) |
156
|
|
|
{ |
157
|
5 |
|
$raw = new \ArrayObject(self::splitStringByLines(file_get_contents($file))); |
158
|
5 |
|
$lineIterator = $raw->getIterator(); |
159
|
5 |
|
$state = 'unchanged'; |
160
|
|
|
$conflictIndicator = [ |
161
|
5 |
|
'<<<<<<< HEAD' => 'local', |
162
|
|
|
'||||||| merged common ancestors' => 'base', |
163
|
|
|
'=======' => 'remote', |
164
|
|
|
'>>>>>>> original' => 'end conflict', |
165
|
|
|
]; |
166
|
|
|
|
167
|
|
|
// Create hunks from the text diff. |
168
|
5 |
|
$differ = new Differ(); |
169
|
5 |
|
$remoteDiff = Line::createArray($differ->diffToArray($baseText, $remoteText)); |
170
|
5 |
|
$localDiff = Line::createArray($differ->diffToArray($baseText, $localText)); |
171
|
|
|
|
172
|
5 |
|
$remote_hunks = new \ArrayObject(Hunk::createArray($remoteDiff)); |
173
|
5 |
|
$local_hunks = new \ArrayObject(Hunk::createArray($localDiff)); |
174
|
|
|
|
175
|
5 |
|
$remoteIterator = $remote_hunks->getIterator(); |
176
|
5 |
|
$localIterator = $local_hunks->getIterator(); |
177
|
|
|
|
178
|
5 |
|
$base = []; |
179
|
5 |
|
$remote = []; |
180
|
5 |
|
$local = []; |
181
|
5 |
|
$lineNumber = -1; |
182
|
5 |
|
$newLine = 0; |
183
|
5 |
|
$skipedLines = 0; |
184
|
5 |
|
$addingConflict = false; |
185
|
|
|
// Loop over all the lines in the file. |
186
|
5 |
|
while ($lineIterator->valid()) { |
187
|
5 |
|
$line = $lineIterator->current(); |
188
|
5 |
|
if (array_key_exists(trim($line), $conflictIndicator)) { |
189
|
|
|
// Check for a line matching a conflict indicator. |
190
|
5 |
|
$state = $conflictIndicator[trim($line)]; |
191
|
5 |
|
$skipedLines++; |
192
|
5 |
|
if ($state == 'end conflict') { |
193
|
|
|
// We just treated a merge conflict. |
194
|
5 |
|
$conflicts[] = new MergeConflict($base, $remote, $local, $lineNumber, $newLine); |
195
|
5 |
|
if ($lineNumber == -1) { |
196
|
1 |
|
$lineNumber = 0; |
197
|
|
|
} |
198
|
5 |
|
$lineNumber += count($base); |
199
|
5 |
|
$newLine += count($remote); |
200
|
5 |
|
$base = []; |
201
|
5 |
|
$remote = []; |
202
|
5 |
|
$local = []; |
203
|
5 |
|
$remoteIterator->next(); |
204
|
5 |
|
$localIterator->next(); |
205
|
|
|
|
206
|
5 |
|
if ($addingConflict) { |
207
|
|
|
// Advance the counter for conflicts with adding. |
208
|
1 |
|
$lineNumber++; |
209
|
1 |
|
$newLine++; |
210
|
1 |
|
$addingConflict = false; |
211
|
|
|
} |
212
|
5 |
|
$state = 'unchanged'; |
213
|
|
|
} |
214
|
|
|
} else { |
215
|
|
|
switch ($state) { |
216
|
5 |
|
case 'local': |
217
|
5 |
|
$local[] = $line; |
218
|
5 |
|
$skipedLines++; |
219
|
5 |
|
break; |
220
|
5 |
|
case 'base': |
221
|
5 |
|
$base[] = $line; |
222
|
5 |
|
$skipedLines++; |
223
|
5 |
|
if ($lineNumber == -1) { |
224
|
1 |
|
$lineNumber = 0; |
225
|
|
|
} |
226
|
5 |
|
break; |
227
|
5 |
|
case 'remote': |
228
|
5 |
|
$remote[] = $line; |
229
|
5 |
|
$merged[] = $line; |
230
|
5 |
|
break; |
231
|
5 |
|
case 'unchanged': |
232
|
5 |
|
if ($lineNumber == -1) { |
233
|
4 |
|
$lineNumber = 0; |
234
|
|
|
} |
235
|
5 |
|
$merged[] = $line; |
236
|
|
|
|
237
|
|
|
/** @var Hunk $r */ |
238
|
5 |
|
$r = $remoteIterator->current(); |
239
|
|
|
/** @var Hunk $l */ |
240
|
5 |
|
$l = $localIterator->current(); |
241
|
|
|
|
242
|
5 |
|
if ($r == $l) { |
243
|
|
|
// If they are the same, treat only one. |
244
|
4 |
|
$localIterator->next(); |
245
|
4 |
|
$l = $localIterator->current(); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
// A hunk has been successfully merged, so we can just |
249
|
|
|
// tally the lines added and removed and skip forward. |
250
|
5 |
|
if ($r && $r->getStart() == $lineNumber) { |
251
|
2 |
|
if (!$r->hasIntersection($l)) { |
252
|
1 |
|
$lineNumber += count($r->getRemovedLines()); |
253
|
1 |
|
$newLine += count($r->getAddedLines()); |
254
|
1 |
|
$lineIterator->seek($newLine + $skipedLines - 1); |
255
|
1 |
|
$remoteIterator->next(); |
256
|
|
|
} else { |
257
|
|
|
// If the conflict occurs on added lines, the |
258
|
|
|
// next line in the merge will deal with it. |
259
|
2 |
|
if ($r->getType() == Hunk::ADDED && $l->getType() == Hunk::ADDED) { |
260
|
1 |
|
$addingConflict = true; |
261
|
|
|
} else { |
262
|
1 |
|
$lineNumber++; |
263
|
2 |
|
$newLine++; |
264
|
|
|
} |
265
|
|
|
} |
266
|
5 |
|
} elseif ($l && $l->getStart() == $lineNumber) { |
267
|
2 |
|
if (!$l->hasIntersection($r)) { |
268
|
1 |
|
$lineNumber += count($l->getRemovedLines()); |
269
|
1 |
|
$newLine += count($l->getAddedLines()); |
270
|
1 |
|
$lineIterator->seek($newLine + $skipedLines - 1); |
271
|
1 |
|
$localIterator->next(); |
272
|
|
|
} else { |
273
|
1 |
|
$lineNumber++; |
274
|
2 |
|
$newLine++; |
275
|
|
|
} |
276
|
|
|
} else { |
277
|
5 |
|
$lineNumber++; |
278
|
5 |
|
$newLine++; |
279
|
|
|
} |
280
|
5 |
|
break; |
281
|
|
|
} |
282
|
|
|
} |
283
|
5 |
|
$lineIterator->next(); |
284
|
|
|
} |
285
|
|
|
|
286
|
5 |
|
$rawBase = self::splitStringByLines($baseText); |
287
|
5 |
|
$lastConflict = end($conflicts); |
288
|
|
|
// Check if the last conflict was at the end of the text. |
289
|
5 |
|
if ($lastConflict->getBaseLine() + count($lastConflict->getBase()) == count($rawBase)) { |
290
|
|
|
// Fix the last lines of all the texts as we can not know from |
291
|
|
|
// the merged text if there was a new line at the end or not. |
292
|
1 |
|
$base = self::fixLastLine($lastConflict->getBase(), $rawBase); |
293
|
1 |
|
$remote = self::fixLastLine($lastConflict->getRemote(), self::splitStringByLines($remoteText)); |
294
|
1 |
|
$local = self::fixLastLine($lastConflict->getLocal(), self::splitStringByLines($localText)); |
295
|
|
|
|
296
|
1 |
|
$newConflict = new MergeConflict( |
297
|
1 |
|
$base, |
298
|
1 |
|
$remote, |
299
|
1 |
|
$local, |
300
|
1 |
|
$lastConflict->getBaseLine(), |
301
|
1 |
|
$lastConflict->getMergedLine() |
302
|
|
|
); |
303
|
1 |
|
$conflicts[key($conflicts)] = $newConflict; |
304
|
|
|
|
305
|
1 |
|
$lastMerged = end($merged); |
306
|
1 |
|
$lastRemote = end($remote); |
307
|
1 |
|
if ($lastMerged !== $lastRemote && rtrim($lastMerged) === $lastRemote) { |
308
|
1 |
|
$merged[key($merged)] = $lastRemote; |
309
|
|
|
} |
310
|
|
|
} |
311
|
5 |
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* @param array $lines |
315
|
|
|
* @param array $all |
316
|
|
|
* |
317
|
|
|
* @return array |
318
|
|
|
*/ |
319
|
1 |
|
protected static function fixLastLine(array $lines, array $all): array |
320
|
|
|
{ |
321
|
1 |
|
$last = end($all); |
322
|
1 |
|
$lastLine = end($lines); |
323
|
1 |
|
if ($lastLine !== false && $last !== $lastLine && rtrim($lastLine) === $last) { |
324
|
1 |
|
$lines[key($lines)] = $last; |
325
|
|
|
} |
326
|
1 |
|
return $lines; |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
/** |
330
|
|
|
* Constructor, not setting anything up. |
331
|
|
|
* |
332
|
|
|
* @param \GitWrapper\GitWrapper $wrapper |
333
|
|
|
*/ |
334
|
11 |
|
public function __construct(GitWrapper $wrapper = null) |
335
|
|
|
{ |
336
|
11 |
|
if (!$wrapper) { |
337
|
11 |
|
$wrapper = new GitWrapper(); |
338
|
|
|
} |
339
|
11 |
|
$this->wrapper = $wrapper; |
340
|
11 |
|
$this->conflict = ''; |
341
|
11 |
|
$this->git = null; |
342
|
11 |
|
$this->dir = null; |
343
|
11 |
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* Set up the git wrapper and the temporary directory. |
347
|
|
|
*/ |
348
|
8 |
|
protected function setup() |
349
|
|
|
{ |
350
|
8 |
|
if (!$this->dir) { |
351
|
|
|
// Greate a temporary directory. |
352
|
8 |
|
$tempfile = tempnam(sys_get_temp_dir(), ''); |
353
|
8 |
|
mkdir($tempfile . '.git'); |
354
|
8 |
|
if (file_exists($tempfile)) { |
355
|
8 |
|
unlink($tempfile); |
356
|
|
|
} |
357
|
8 |
|
$this->dir = $tempfile . '.git'; |
358
|
8 |
|
$this->git = $this->wrapper->init($this->dir); |
359
|
|
|
} |
360
|
8 |
|
if ($this->git) { |
361
|
8 |
|
$this->git->config('user.name', 'GitMerge'); |
362
|
8 |
|
$this->git->config('user.email', '[email protected]'); |
363
|
8 |
|
$this->git->config('merge.conflictStyle', 'diff3'); |
364
|
|
|
} |
365
|
8 |
|
} |
366
|
|
|
|
367
|
|
|
/** |
368
|
|
|
* Clean the temporary directory used for merging. |
369
|
|
|
*/ |
370
|
1 |
|
protected function cleanup() |
371
|
|
|
{ |
372
|
1 |
|
if (is_dir($this->dir)) { |
373
|
|
|
// Recursively delete all files and folders. |
374
|
1 |
|
$files = new \RecursiveIteratorIterator( |
375
|
1 |
|
new \RecursiveDirectoryIterator($this->dir, \RecursiveDirectoryIterator::SKIP_DOTS), |
376
|
1 |
|
\RecursiveIteratorIterator::CHILD_FIRST |
377
|
|
|
); |
378
|
|
|
|
379
|
1 |
|
foreach ($files as $fileinfo) { |
380
|
1 |
|
if ($fileinfo->isDir()) { |
381
|
1 |
|
rmdir($fileinfo->getRealPath()); |
382
|
|
|
} else { |
383
|
1 |
|
unlink($fileinfo->getRealPath()); |
384
|
|
|
} |
385
|
|
|
} |
386
|
1 |
|
rmdir($this->dir); |
387
|
1 |
|
unset($this->git); |
388
|
|
|
} |
389
|
1 |
|
} |
390
|
|
|
|
391
|
|
|
/** |
392
|
|
|
* Clean up the temporary git directory. |
393
|
|
|
*/ |
394
|
1 |
|
public function __destruct() |
395
|
|
|
{ |
396
|
1 |
|
$this->cleanup(); |
397
|
1 |
|
} |
398
|
|
|
} |
399
|
|
|
|
Scrutinizer analyzes your
composer.json
/composer.lock
file if available to determine the classes, and functions that are defined by your dependencies.It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.