PhpMerge::mergeHunks()   F
last analyzed

Complexity

Conditions 23
Paths 181

Size

Total Lines 69
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 38
CRAP Score 23

Importance

Changes 0
Metric Value
cc 23
eloc 39
nc 181
nop 4
dl 0
loc 69
ccs 38
cts 38
cp 1
crap 23
rs 3.4916
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 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