Remote   D
last analyzed

Complexity

Total Complexity 59

Size/Duplication

Total Lines 484
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 95.6%

Importance

Changes 0
Metric Value
wmc 59
lcom 1
cbo 3
dl 0
loc 484
ccs 152
cts 159
cp 0.956
rs 4.08
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A pick() 0 7 1
A getVerboseOutput() 0 9 2
A getShowOutput() 0 12 2
A createFromCommand() 0 20 4
C parseOutputLines() 0 50 16
B aggregateBranchDetails() 0 33 8
A parseRemoteBranches() 0 15 3
A parseLocalPullBranches() 0 15 3
A parseLocalPushRefs() 0 16 3
A parseName() 0 11 2
A getMatches() 0 10 2
A __toString() 0 4 1
A setName() 0 4 1
A getName() 0 4 1
A getFetchURL() 0 4 1
A setFetchURL() 0 4 1
A getPushURL() 0 4 1
A setPushURL() 0 4 1
A getRemoteHEAD() 0 4 1
A setRemoteHEAD() 0 4 1
A getBranches() 0 4 1
A setBranches() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Remote often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Remote, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * GitElephant - An abstraction layer for git written in PHP
5
 * Copyright (C) 2013  Matteo Giachino
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program.  If not, see [http://www.gnu.org/licenses/].
19
 */
20
21
namespace GitElephant\Objects;
22
23
use GitElephant\Command\RemoteCommand;
24
use GitElephant\Repository;
25
26
/**
27
 * Class Remote
28
 *
29
 * An object representing a git remote
30
 *
31
 * @package GitElephant\Objects
32
 * @author  David Neimeyer <[email protected]>
33
 */
34
class Remote
35
{
36
    /**
37
     * @var \GitElephant\Repository
38
     */
39
    private $repository;
40
41
    /**
42
     * remote name
43
     *
44
     * @var string
45
     */
46
    private $name;
47
48
    /**
49
     * fetch url of named remote
50
     *
51
     * @var string
52
     */
53
    private $fetchURL = '';
54
55
    /**
56
     * push url of named remote
57
     *
58
     * @var string
59
     */
60
    private $pushURL = '';
61
62
    /**
63
     * HEAD branch of named remote
64
     *
65
     * @var string
66
     */
67
    private $remoteHEAD = null;
68
69
    /**
70
     * @var array<Branch>
71
     */
72
    private $branches;
73
74
    /**
75
     * Class constructor
76
     *
77
     * @param \GitElephant\Repository $repository   repository instance
78
     * @param string|null                  $name         remote name
79
     * @param bool                    $queryRemotes Do not fetch new information from remotes
80
     *
81
     * @throws \RuntimeException
82
     * @throws \InvalidArgumentException
83
     * @throws \UnexpectedValueException
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 14
    }
93
94
    /**
95
     * Static constructor
96
     *
97
     * @param \GitElephant\Repository $repository   repository instance
98
     * @param string                  $name         remote name
99
     * @param bool                    $queryRemotes Fetch new information from remotes
100
     *
101
     * @return \GitElephant\Objects\Remote
102
     */
103 1
    public static function pick(
104
        Repository $repository,
105
        string $name = null,
106
        bool $queryRemotes = true
107
    ): \GitElephant\Objects\Remote {
108 1
        return new self($repository, $name, $queryRemotes);
109
    }
110
111
    /**
112
     * get output lines from git-remote --verbose
113
     *
114
     * @param RemoteCommand $remoteCmd Optionally provide RemoteCommand object
115
     *
116
     * @throws \RuntimeException
117
     * @throws \Symfony\Component\Process\Exception\LogicException
118
     * @throws \Symfony\Component\Process\Exception\InvalidArgumentException
119
     * @throws \Symfony\Component\Process\Exception\RuntimeException
120
     * @return array<string>
121
     */
122 2
    public function getVerboseOutput(RemoteCommand $remoteCmd = null): array
123
    {
124 2
        if ($remoteCmd === null) {
125 2
            $remoteCmd = RemoteCommand::getInstance($this->repository);
126
        }
127 2
        $command = $remoteCmd->verbose();
128
129 2
        return $this->repository->getCaller()->execute($command)->getOutputLines(true);
130
    }
131
132
    /**
133
     * get output lines from git-remote show [name]
134
     *
135
     * NOTE: for technical reasons $name is optional, however under normal
136
     * implementation it SHOULD be passed!
137
     *
138
     * @param string        $name         Name of remote to show details
139
     * @param RemoteCommand $remoteCmd    Optionally provide RemoteCommand object
140
     * @param bool          $queryRemotes Do not fetch new information from remotes
141
     *
142
     * @throws \RuntimeException
143
     * @throws \Symfony\Component\Process\Exception\LogicException
144
     * @throws \Symfony\Component\Process\Exception\InvalidArgumentException
145
     * @throws \Symfony\Component\Process\Exception\RuntimeException
146
     * @return array<string>
147
     */
148 2
    public function getShowOutput(
149
        string $name = null,
150
        RemoteCommand $remoteCmd = null,
151
        bool $queryRemotes = true
152
    ): array {
153 2
        if ($remoteCmd === null) {
154 2
            $remoteCmd = RemoteCommand::getInstance($this->repository);
155
        }
156 2
        $command = $remoteCmd->show($name, $queryRemotes);
157
158 2
        return $this->repository->getCaller()->execute($command)->getOutputLines(true);
159
    }
160
161
    /**
162
     * get/store the properties from git-remote command
163
     *
164
     * NOTE: the name property should be set if this is to do anything,
165
     * otherwise it's likely to throw
166
     *
167
     * @param bool $queryRemotes Do not fetch new information from remotes
168
     *
169
     * @throws \RuntimeException
170
     * @throws \UnexpectedValueException
171
     * @throws \InvalidArgumentException
172
     * @throws \Symfony\Component\Process\Exception\RuntimeException
173
     * @return \GitElephant\Objects\Remote
174
     */
175 3
    private function createFromCommand(bool $queryRemotes = true): self
176
    {
177 3
        $outputLines = $this->getVerboseOutput();
178 3
        $list = [];
179 3
        foreach ($outputLines as $line) {
180 3
            $matches = static::getMatches($line);
181 3
            if (isset($matches[1])) {
182 3
                $list[] = $matches[1];
183
            }
184
        }
185 3
        array_filter($list);
186 3
        if (in_array($this->name, $list)) {
187 3
            $remoteDetails = $this->getShowOutput($this->name, null, $queryRemotes);
188 3
            $this->parseOutputLines($remoteDetails);
189
        } else {
190
            throw new \InvalidArgumentException(sprintf('The %s remote doesn\'t exists', $this->name));
191
        }
192
193 3
        return $this;
194
    }
195
196
    /**
197
     * parse details from remote show
198
     *
199
     * @param array $remoteDetails Output lines for a remote show
200
     *
201
     * @throws \UnexpectedValueException
202
     */
203 8
    public function parseOutputLines(array $remoteDetails): void
204
    {
205 8
        array_filter($remoteDetails);
206 8
        $name = array_shift($remoteDetails);
207 8
        $name = is_string($name) ? trim($name) : '';
208 8
        $name = $this->parseName($name);
209
210 8
        if ($name === '') {
211
            throw new \UnexpectedValueException(sprintf('Invalid data provided for remote detail parsing'));
212
        }
213
214 8
        $this->name = $name;
215 8
        $fetchURLPattern = '/^Fetch\s+URL:\s*(.*)$/';
216 8
        $fetchURL = null;
217
218 8
        $pushURLPattern = '/^Push\s+URL:\s*(.*)$/';
219 8
        $pushURL = null;
220
221 8
        $remoteHEADPattern = '/^HEAD\s+branch:\s*(.*)$/';
222 8
        $remoteHEAD = null;
223
224 8
        $remoteBranchHeaderPattern = '/^Remote\s+branch(?:es)?:$/';
225 8
        $localBranchPullHeaderPattern = '/^Local\sbranch(?:es)?\sconfigured\sfor\s\'git\spull\'\:$/';
226 8
        $localRefPushHeaderPattern = '/^Local\sref(?:s)?\sconfigured\sfor\s\'git\spush\':$/';
227
        $groups = [
228 8
            'remoteBranches' => null,
229
            'localBranches' => null,
230
            'localRefs' => null,
231
        ];
232
233 8
        foreach ($remoteDetails as $lineno => $line) {
234 8
            $line = trim($line);
235 8
            $matches = [];
236 8
            if (is_null($fetchURL) && preg_match($fetchURLPattern, $line, $matches)) {
237 8
                $this->fetchURL = $fetchURL = $matches[1];
238 8
            } elseif (is_null($pushURL) && preg_match($pushURLPattern, $line, $matches)) {
239 8
                $this->pushURL = $pushURL = $matches[1];
240 8
            } elseif (is_null($remoteHEAD) && preg_match($remoteHEADPattern, $line, $matches)) {
241 8
                $this->remoteHEAD = $remoteHEAD = $matches[1];
242 7
            } elseif (is_null($groups['remoteBranches']) && preg_match($remoteBranchHeaderPattern, $line, $matches)) {
243 7
                $groups['remoteBranches'] = $lineno;
244 7
            } elseif (is_null($groups['localBranches']) && preg_match($localBranchPullHeaderPattern, $line, $matches)) {
245 7
                $groups['localBranches'] = $lineno;
246 7
            } elseif (is_null($groups['localRefs']) && preg_match($localRefPushHeaderPattern, $line, $matches)) {
247 7
                $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<array<string>>
263
     */
264 8
    protected function aggregateBranchDetails($groupLines, $remoteDetails): array
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] += $data;
292
            }
293
        }
294
295 8
        return $aggBranches;
296
    }
297
298
    /**
299
     * parse the details related to remote branch references
300
     *
301
     * @param array<string> $lines
302
     *
303
     * @return array // <string, array<string, string>>
304
     */
305 8
    public function parseRemoteBranches(array $lines): array
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<string> $lines
326
     *
327
     * @return array // <array<string>>
328
     */
329 8
    public function parseLocalPullBranches($lines): array
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<string> $lines
350
     *
351
     * @return array // <array<string>>
352
     */
353 8
    public function parseLocalPushRefs($lines): array
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(string $remoteString): array
398
    {
399 3
        $matches = [];
400 3
        preg_match('/^(\S+)\s*(\S[^\( ]+)\s*\((.+)\)$/', trim($remoteString), $matches);
401 3
        if (empty($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(): string
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): void
424
    {
425 1
        $this->name = $name;
426 1
    }
427
428
    /**
429
     * name getter
430
     *
431
     * @return string
432
     */
433 4
    public function getName(): string
434
    {
435 4
        return $this->name;
436
    }
437
438
    /**
439
     * fetchURL getter
440
     *
441
     * @return string
442
     */
443 2
    public function getFetchURL(): string
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): void
454
    {
455 1
        $this->fetchURL = $url;
456 1
    }
457
458
    /**
459
     * pushURL getter
460
     *
461
     * @return string
462
     */
463 2
    public function getPushURL(): string
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): void
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(): string
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): void
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(): array
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): void
514
    {
515 8
        $this->branches = $branches;
516 8
    }
517
}
518