Completed
Pull Request — develop (#140)
by
unknown
08:01 queued 02:05
created

Remote::parseOutputLines()   C

Complexity

Conditions 16
Paths 18

Size

Total Lines 53
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 16.0059

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 53
ccs 34
cts 35
cp 0.9714
rs 6.2752
cc 16
eloc 37
nc 18
nop 1
crap 16.0059

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
 * GitElephant - An abstraction layer for git written in PHP
4
 * Copyright (C) 2013  Matteo Giachino
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program.  If not, see [http://www.gnu.org/licenses/].
18
 */
19
20
namespace GitElephant\Objects;
21
22
use \GitElephant\Command\RemoteCommand;
23
use \GitElephant\Repository;
24
25
/**
26
 * Class Remote
27
 *
28
 * An object representing a git remote
29
 *
30
 * @package GitElephant\Objects
31
 * @author  David Neimeyer <[email protected]>
32
 */
33
class Remote
34
{
35
    /**
36
     * @var \GitElephant\Repository
37
     */
38
    private $repository;
39
40
    /**
41
     * remote name
42
     *
43
     * @var string
44
     */
45
    private $name;
46
47
    /**
48
     * fetch url of named remote
49
     *
50
     * @var string
51
     */
52
    private $fetchURL = '';
53
54
    /**
55
     * push url of named remote
56
     *
57
     * @var string
58
     */
59
    private $pushURL = '';
60
61
    /**
62
     * HEAD branch of named remote
63
     *
64
     * @var string
65
     */
66
    private $remoteHEAD = null;
67
68
    /**
69
     * @var array
70
     */
71
    private $branches;
72
73
    /**
74
     * Class constructor
75
     *
76
     * @param \GitElephant\Repository $repository   repository instance
77
     * @param string                  $name         remote name
78
     * @param bool                    $queryRemotes Do not fetch new information from remotes
79
     *
80
     * @throws \RuntimeException
81
     * @throws \InvalidArgumentException
82
     * @throws \UnexpectedValueException
83
     * @return \GitElephant\Objects\Remote
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
84
     */
85 14
    public function __construct(Repository $repository, string $name = null, bool $queryRemotes = true)
86
    {
87 14
        $this->repository = $repository;
88 14
        if ($name) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $name of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
89 3
            $this->name = trim($name);
90 3
            $this->createFromCommand($queryRemotes);
91
        }
92
93 14
        return $this;
0 ignored issues
show
Bug introduced by
Constructors do not have meaningful return values, anything that is returned from here is discarded. Are you sure this is correct?
Loading history...
94
    }
95
96
    /**
97
     * Static constructor
98
     *
99
     * @param \GitElephant\Repository $repository   repository instance
100
     * @param string                  $name         remote name
101
     * @param bool                    $queryRemotes Fetch new information from remotes
102
     *
103
     * @return \GitElephant\Objects\Remote
104
     */
105 1
    public static function pick(Repository $repository, string $name = null, bool $queryRemotes = true)
106
    {
107 1
        return new self($repository, $name, $queryRemotes);
108
    }
109
110
    /**
111
     * get output lines from git-remote --verbose
112
     *
113
     * @param RemoteCommand $remoteCmd Optionally provide RemoteCommand object
114
     *
115
     * @throws \RuntimeException
116
     * @throws \Symfony\Component\Process\Exception\LogicException
117
     * @throws \Symfony\Component\Process\Exception\InvalidArgumentException
118
     * @throws \Symfony\Component\Process\Exception\RuntimeException
119
     * @return array
120
     */
121 2
    public function getVerboseOutput(RemoteCommand $remoteCmd = null)
122
    {
123 2
        if (!$remoteCmd) {
124 2
            $remoteCmd = RemoteCommand::getInstance($this->repository);
125
        }
126 2
        $command = $remoteCmd->verbose();
127
128 2
        return $this->repository->getCaller()->execute($command)->getOutputLines(true);
129
    }
130
131
    /**
132
     * get output lines from git-remote show [name]
133
     *
134
     * NOTE: for technical reasons $name is optional, however under normal
135
     * implementation it SHOULD be passed!
136
     *
137
     * @param string        $name         Name of remote to show details
138
     * @param RemoteCommand $remoteCmd    Optionally provide RemoteCommand object
139
     * @param bool          $queryRemotes Do not fetch new information from remotes
140
     *
141
     * @throws \RuntimeException
142
     * @throws \Symfony\Component\Process\Exception\LogicException
143
     * @throws \Symfony\Component\Process\Exception\InvalidArgumentException
144
     * @throws \Symfony\Component\Process\Exception\RuntimeException
145
     * @return array
146
     */
147 2
    public function getShowOutput(string $name = null, RemoteCommand $remoteCmd = null, bool $queryRemotes = true)
148
    {
149 2
        if (!$remoteCmd) {
150 2
            $remoteCmd = RemoteCommand::getInstance($this->repository);
151
        }
152 2
        $command = $remoteCmd->show($name, $queryRemotes);
153
154 2
        return $this->repository->getCaller()->execute($command)->getOutputLines(true);
155
    }
156
157
    /**
158
     * get/store the properties from git-remote command
159
     *
160
     * NOTE: the name property should be set if this is to do anything,
161
     * otherwise it's likely to throw
162
     *
163
     * @param bool $queryRemotes Do not fetch new information from remotes
164
     *
165
     * @throws \RuntimeException
166
     * @throws \UnexpectedValueException
167
     * @throws \InvalidArgumentException
168
     * @throws \Symfony\Component\Process\Exception\RuntimeException
169
     * @return \GitElephant\Objects\Remote
170
     */
171 3
    private function createFromCommand(bool $queryRemotes = true)
172
    {
173 3
        $outputLines = $this->getVerboseOutput();
174 3
        $list = [];
175 3
        foreach ($outputLines as $line) {
176 3
            $matches = static::getMatches($line);
177 3
            if (isset($matches[1])) {
178 3
                $list[] = $matches[1];
179
            }
180
        }
181 3
        array_filter($list);
182 3
        if (in_array($this->name, $list)) {
183 3
            $remoteDetails = $this->getShowOutput($this->name, null, $queryRemotes);
184 3
            $this->parseOutputLines($remoteDetails);
185
        }
186
        else {
187
            throw new \InvalidArgumentException(sprintf('The %s remote doesn\'t exists', $this->name));
188
        }
189
190 3
        return $this;
191
    }
192
193
    /**
194
     * parse details from remote show
195
     *
196
     * @param array|string $remoteDetails Output lines for a remote show
197
     *
198
     * @throws \UnexpectedValueException
199
     */
200 8
    public function parseOutputLines(array $remoteDetails)
201
    {
202 8
        array_filter($remoteDetails);
203 8
        $name = array_shift($remoteDetails);
204 8
        $name = (is_string($name)) ? trim($name) : '';
205 8
        $name = $this->parseName($name);
206 8
        if (!$name) {
207
            throw new \UnexpectedValueException(sprintf('Invalid data provided for remote detail parsing'));
208
        }
209 8
        $this->name = $name;
210 8
        $fetchURLPattern = '/^Fetch\s+URL:\s*(.*)$/';
211 8
        $fetchURL = null;
212
213 8
        $pushURLPattern = '/^Push\s+URL:\s*(.*)$/';
214 8
        $pushURL = null;
215
216 8
        $remoteHEADPattern = '/^HEAD\s+branch:\s*(.*)$/';
217 8
        $remoteHEAD = null;
218
219 8
        $remoteBranchHeaderPattern = '/^Remote\s+branch(?:es)?:$/';
220 8
        $localBranchPullHeaderPattern = '/^Local\sbranch(?:es)?\sconfigured\sfor\s\'git\spull\'\:$/';
221 8
        $localRefPushHeaderPattern = '/^Local\sref(?:s)?\sconfigured\sfor\s\'git\spush\':$/';
222
        $groups = [
223 8
            'remoteBranches' => null,
224
            'localBranches'  => null,
225
            'localRefs'      => null,
226
        ];
227
228 8
        foreach ($remoteDetails as $lineno => $line) {
229 8
            $line = trim($line);
230 8
            $matches = [];
231 8
            if (is_null($fetchURL) && preg_match($fetchURLPattern, $line, $matches)) {
232 8
                $this->fetchURL = $fetchURL = $matches[1];
233
            }
234 8
            elseif (is_null($pushURL) && preg_match($pushURLPattern, $line, $matches)) {
235 8
                $this->pushURL = $pushURL = $matches[1];
236
            }
237 8
            elseif (is_null($remoteHEAD) && preg_match($remoteHEADPattern, $line, $matches)) {
238 8
                $this->remoteHEAD = $remoteHEAD = $matches[1];
239
            }
240 7
            elseif (is_null($groups['remoteBranches']) && preg_match($remoteBranchHeaderPattern, $line, $matches)) {
241 7
                $groups['remoteBranches'] = $lineno;
242
            }
243 7
            elseif (is_null($groups['localBranches']) && preg_match($localBranchPullHeaderPattern, $line, $matches)) {
244 7
                $groups['localBranches'] = $lineno;
245
            }
246 7
            elseif (is_null($groups['localRefs']) && preg_match($localRefPushHeaderPattern, $line, $matches)) {
247 8
                $groups['localRefs'] = $lineno;
248
            }
249
        }
250
251 8
        $this->setBranches($this->aggregateBranchDetails($groups, $remoteDetails));
252 8
    }
253
254
    /**
255
     * provided with the start points of the branch details, parse out the
256
     * branch details and return a structured representation of said details
257
     *
258
     * @param array $groupLines    Associative array whose values are line numbers
259
     *                             are respective of the "group" detail present in $remoteDetails
260
     * @param array $remoteDetails Output of git-remote show [name]
261
     *
262
     * @return array
263
     */
264 8
    protected function aggregateBranchDetails($groupLines, $remoteDetails)
265
    {
266 8
        $configuredRefs = [];
267 8
        arsort($groupLines);
268 8
        foreach ($groupLines as $type => $lineno) {
269 8
            $configuredRefs[$type] = array_splice($remoteDetails, $lineno);
270 8
            array_shift($configuredRefs[$type]);
271
        }
272
273 8
        $configuredRefs['remoteBranches'] = isset($configuredRefs['remoteBranches'])
274 8
            ? $this->parseRemoteBranches($configuredRefs['remoteBranches'])
275
            : [];
276
277 8
        $configuredRefs['localBranches'] = isset($configuredRefs['localBranches'])
278 8
            ? $this->parseLocalPullBranches($configuredRefs['localBranches'])
279
            : [];
280
281 8
        $configuredRefs['localRefs'] = isset($configuredRefs['localRefs'])
282 8
            ? $this->parseLocalPushRefs($configuredRefs['localRefs'])
283
            : [];
284
285 8
        $aggBranches = [];
286 8
        foreach ($configuredRefs as $branches) {
287 8
            foreach ($branches as $branchName => $data) {
288 8
                if (!isset($aggBranches[$branchName])) {
289 8
                    $aggBranches[$branchName] = [];
290
                }
291 8
                $aggBranches[$branchName] = $aggBranches[$branchName] + $data;
292
            }
293
        }
294
295 8
        return $aggBranches;
296
    }
297
298
    /**
299
     * parse the details related to remote branch references
300
     *
301
     * @param array $lines
302
     *
303
     * @return array
304
     */
305 8
    public function parseRemoteBranches(array $lines)
306
    {
307 8
        $branches = [];
308 8
        $delimiter = ' ';
309 8
        foreach ($lines as $line) {
310 8
            $line = trim($line);
311 8
            $line = preg_replace('/\s+/', ' ', $line);
312 8
            $parts = explode($delimiter, $line);
313 8
            if (count($parts) > 1) {
314 8
                $branches[$parts[0]] = ['local_relationship' => $parts[1]];
315
            }
316
        }
317
318 8
        return $branches;
319
    }
320
321
    /**
322
     * parse the details related to local branches and the remotes that they
323
     * merge with
324
     *
325
     * @param array $lines
326
     *
327
     * @return array
328
     */
329 8
    public function parseLocalPullBranches($lines)
330
    {
331 8
        $branches = [];
332 8
        $delimiter = ' merges with remote ';
333 8
        foreach ($lines as $line) {
334 7
            $line = trim($line);
335 7
            $line = preg_replace('/\s+/', ' ', $line);
336 7
            $parts = explode($delimiter, $line);
337 7
            if (count($parts) > 1) {
338 7
                $branches[$parts[0]] = ['merges_with' => $parts[1]];
339
            }
340
        }
341
342 8
        return $branches;
343
    }
344
345
    /**
346
     * parse the details related to local branches and the remotes that they
347
     * push to
348
     *
349
     * @param array $lines
350
     *
351
     * @return array
352
     */
353 8
    public function parseLocalPushRefs($lines)
354
    {
355 8
        $branches = [];
356 8
        $delimiter = ' pushes to ';
357 8
        foreach ($lines as $line) {
358 7
            $line = trim($line);
359 7
            $line = preg_replace('/\s+/', ' ', $line);
360 7
            $parts = explode($delimiter, $line);
361 7
            if (count($parts) > 1) {
362 7
                $value = explode(' ', $parts[1], 2);
363 7
                $branches[$parts[0]] = ['pushes_to' => $value[0], 'local_state' => $value[1]];
364
            }
365
        }
366
367 8
        return $branches;
368
    }
369
370
    /**
371
     * parse remote name from git-remote show [name] output line
372
     *
373
     * @param string $line
374
     *
375
     * @return string remote name or blank if invalid
376
     */
377 8
    public function parseName($line)
378
    {
379 8
        $matches = [];
380 8
        $pattern = '/^\*\s+remote\s+(.*)$/';
381 8
        preg_match($pattern, trim($line), $matches);
382 8
        if (!isset($matches[1])) {
383
            return '';
384
        }
385
386 8
        return $matches[1];
387
    }
388
389
    /**
390
     * get the matches from an output line
391
     *
392
     * @param string $remoteString remote line output
393
     *
394
     * @throws \InvalidArgumentException
395
     * @return array
396
     */
397 3
    public static function getMatches($remoteString)
398
    {
399 3
        $matches = [];
400 3
        preg_match('/^(\S+)\s*(\S[^\( ]+)\s*\((.+)\)$/', trim($remoteString), $matches);
401 3
        if (!count($matches)) {
402
            throw new \InvalidArgumentException(sprintf('the remote string is not valid: %s', $remoteString));
403
        }
404
405 3
        return array_map('trim', $matches);
406
    }
407
408
    /**
409
     * toString magic method
410
     *
411
     * @return string the named remote
412
     */
413 1
    public function __toString()
414
    {
415 1
        return $this->getName();
416
    }
417
418
    /**
419
     * name setter
420
     *
421
     * @param string $name the remote name
422
     */
423 1
    public function setName($name)
424
    {
425 1
        $this->name = $name;
426 1
    }
427
428
    /**
429
     * name getter
430
     *
431
     * @return string
432
     */
433 4
    public function getName()
434
    {
435 4
        return $this->name;
436
    }
437
438
    /**
439
     * fetchURL getter
440
     *
441
     * @return string
442
     */
443 2
    public function getFetchURL()
444
    {
445 2
        return $this->fetchURL;
446
    }
447
448
    /**
449
     * fetchURL setter
450
     *
451
     * @param string $url the fetch url
452
     */
453 1
    public function setFetchURL($url)
454
    {
455 1
        $this->fetchURL = $url;
456 1
    }
457
458
    /**
459
     * pushURL getter
460
     *
461
     * @return string
462
     */
463 2
    public function getPushURL()
464
    {
465 2
        return $this->pushURL;
466
    }
467
468
    /**
469
     * pushURL setter
470
     *
471
     * @param string $url the push url
472
     */
473 1
    public function setPushURL($url)
474
    {
475 1
        $this->pushURL = $url;
476 1
    }
477
478
    /**
479
     * remote HEAD branch getter
480
     *
481
     * @return string
482
     */
483 2
    public function getRemoteHEAD()
484
    {
485 2
        return $this->remoteHEAD;
486
    }
487
488
    /**
489
     * remote HEAD branch setter
490
     *
491
     * @param string $branchName
492
     */
493 1
    public function setRemoteHEAD($branchName)
494
    {
495 1
        $this->remoteHEAD = $branchName;
496 1
    }
497
498
    /**
499
     * get structured representation of branches
500
     *
501
     * @return array
502
     */
503 1
    public function getBranches()
504
    {
505 1
        return $this->branches;
506
    }
507
508
    /**
509
     * set structured representation of branches
510
     *
511
     * @param array $branches
512
     */
513 8
    public function setBranches(Array $branches)
0 ignored issues
show
Coding Style introduced by
As per coding-style, PHP keywords should be in lowercase; expected array, but found Array.
Loading history...
514
    {
515 8
        $this->branches = $branches;
516 8
    }
517
}
518