GitMerge::getConflicts()   F
last analyzed

Complexity

Conditions 24
Paths 105

Size

Total Lines 154
Code Lines 111

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 102
CRAP Score 24

Importance

Changes 0
Metric Value
cc 24
eloc 111
nc 105
nop 6
dl 0
loc 154
ccs 102
cts 102
cp 1
crap 24
rs 3.3
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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