Completed
Push — develop ( 5247fd...3c6b2e )
by
unknown
9s
created

Remote::parseLocalPullBranches()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 10
cts 10
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 10
nc 3
nop 1
crap 3
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
        } else {
186
            throw new \InvalidArgumentException(sprintf('The %s remote doesn\'t exists', $this->name));
187
        }
188
189 3
        return $this;
190
    }
191
192
    /**
193
     * parse details from remote show
194
     *
195
     * @param array|string $remoteDetails Output lines for a remote show
196
     *
197
     * @throws \UnexpectedValueException
198
     */
199 8
    public function parseOutputLines(array $remoteDetails)
200
    {
201 8
        array_filter($remoteDetails);
202 8
        $name = array_shift($remoteDetails);
203 8
        $name = (is_string($name)) ? trim($name) : '';
204 8
        $name = $this->parseName($name);
205 8
        if (!$name) {
206
            throw new \UnexpectedValueException(sprintf('Invalid data provided for remote detail parsing'));
207
        }
208 8
        $this->name = $name;
209 8
        $fetchURLPattern = '/^Fetch\s+URL:\s*(.*)$/';
210 8
        $fetchURL = null;
211
212 8
        $pushURLPattern = '/^Push\s+URL:\s*(.*)$/';
213 8
        $pushURL = null;
214
215 8
        $remoteHEADPattern = '/^HEAD\s+branch:\s*(.*)$/';
216 8
        $remoteHEAD = null;
217
218 8
        $remoteBranchHeaderPattern = '/^Remote\s+branch(?:es)?:$/';
219 8
        $localBranchPullHeaderPattern = '/^Local\sbranch(?:es)?\sconfigured\sfor\s\'git\spull\'\:$/';
220 8
        $localRefPushHeaderPattern = '/^Local\sref(?:s)?\sconfigured\sfor\s\'git\spush\':$/';
221
        $groups = [
222 8
            'remoteBranches' => null,
223
            'localBranches'  => null,
224
            'localRefs'      => null,
225
        ];
226
227 8
        foreach ($remoteDetails as $lineno => $line) {
228 8
            $line = trim($line);
229 8
            $matches = [];
230 8
            if (is_null($fetchURL) && preg_match($fetchURLPattern, $line, $matches)) {
231 8
                $this->fetchURL = $fetchURL = $matches[1];
232 8
            } elseif (is_null($pushURL) && preg_match($pushURLPattern, $line, $matches)) {
233 8
                $this->pushURL = $pushURL = $matches[1];
234 8
            } elseif (is_null($remoteHEAD) && preg_match($remoteHEADPattern, $line, $matches)) {
235 8
                $this->remoteHEAD = $remoteHEAD = $matches[1];
236 7
            } elseif (is_null($groups['remoteBranches']) && preg_match($remoteBranchHeaderPattern, $line, $matches)) {
237 7
                $groups['remoteBranches'] = $lineno;
238 7
            } elseif (is_null($groups['localBranches']) && preg_match($localBranchPullHeaderPattern, $line, $matches)) {
239 7
                $groups['localBranches'] = $lineno;
240 7
            } elseif (is_null($groups['localRefs']) && preg_match($localRefPushHeaderPattern, $line, $matches)) {
241 8
                $groups['localRefs'] = $lineno;
242
            }
243
        }
244
245 8
        $this->setBranches($this->aggregateBranchDetails($groups, $remoteDetails));
246 8
    }
247
248
    /**
249
     * provided with the start points of the branch details, parse out the
250
     * branch details and return a structured representation of said details
251
     *
252
     * @param array $groupLines    Associative array whose values are line numbers
253
     *                             are respective of the "group" detail present in $remoteDetails
254
     * @param array $remoteDetails Output of git-remote show [name]
255
     *
256
     * @return array
257
     */
258 8
    protected function aggregateBranchDetails($groupLines, $remoteDetails)
259
    {
260 8
        $configuredRefs = [];
261 8
        arsort($groupLines);
262 8
        foreach ($groupLines as $type => $lineno) {
263 8
            $configuredRefs[$type] = array_splice($remoteDetails, $lineno);
264 8
            array_shift($configuredRefs[$type]);
265
        }
266
267 8
        $configuredRefs['remoteBranches'] = isset($configuredRefs['remoteBranches'])
268 8
            ? $this->parseRemoteBranches($configuredRefs['remoteBranches'])
269
            : [];
270
271 8
        $configuredRefs['localBranches'] = isset($configuredRefs['localBranches'])
272 8
            ? $this->parseLocalPullBranches($configuredRefs['localBranches'])
273
            : [];
274
275 8
        $configuredRefs['localRefs'] = isset($configuredRefs['localRefs'])
276 8
            ? $this->parseLocalPushRefs($configuredRefs['localRefs'])
277
            : [];
278
279 8
        $aggBranches = [];
280 8
        foreach ($configuredRefs as $branches) {
281 8
            foreach ($branches as $branchName => $data) {
282 8
                if (!isset($aggBranches[$branchName])) {
283 8
                    $aggBranches[$branchName] = [];
284
                }
285 8
                $aggBranches[$branchName] = $aggBranches[$branchName] + $data;
286
            }
287
        }
288
289 8
        return $aggBranches;
290
    }
291
292
    /**
293
     * parse the details related to remote branch references
294
     *
295
     * @param array $lines
296
     *
297
     * @return array
298
     */
299 8
    public function parseRemoteBranches(array $lines)
300
    {
301 8
        $branches = [];
302 8
        $delimiter = ' ';
303 8
        foreach ($lines as $line) {
304 8
            $line = trim($line);
305 8
            $line = preg_replace('/\s+/', ' ', $line);
306 8
            $parts = explode($delimiter, $line);
307 8
            if (count($parts) > 1) {
308 8
                $branches[$parts[0]] = ['local_relationship' => $parts[1]];
309
            }
310
        }
311
312 8
        return $branches;
313
    }
314
315
    /**
316
     * parse the details related to local branches and the remotes that they
317
     * merge with
318
     *
319
     * @param array $lines
320
     *
321
     * @return array
322
     */
323 8
    public function parseLocalPullBranches($lines)
324
    {
325 8
        $branches = [];
326 8
        $delimiter = ' merges with remote ';
327 8
        foreach ($lines as $line) {
328 7
            $line = trim($line);
329 7
            $line = preg_replace('/\s+/', ' ', $line);
330 7
            $parts = explode($delimiter, $line);
331 7
            if (count($parts) > 1) {
332 7
                $branches[$parts[0]] = ['merges_with' => $parts[1]];
333
            }
334
        }
335
336 8
        return $branches;
337
    }
338
339
    /**
340
     * parse the details related to local branches and the remotes that they
341
     * push to
342
     *
343
     * @param array $lines
344
     *
345
     * @return array
346
     */
347 8
    public function parseLocalPushRefs($lines)
348
    {
349 8
        $branches = [];
350 8
        $delimiter = ' pushes to ';
351 8
        foreach ($lines as $line) {
352 7
            $line = trim($line);
353 7
            $line = preg_replace('/\s+/', ' ', $line);
354 7
            $parts = explode($delimiter, $line);
355 7
            if (count($parts) > 1) {
356 7
                $value = explode(' ', $parts[1], 2);
357 7
                $branches[$parts[0]] = ['pushes_to' => $value[0], 'local_state' => $value[1]];
358
            }
359
        }
360
361 8
        return $branches;
362
    }
363
364
    /**
365
     * parse remote name from git-remote show [name] output line
366
     *
367
     * @param string $line
368
     *
369
     * @return string remote name or blank if invalid
370
     */
371 8
    public function parseName($line)
372
    {
373 8
        $matches = [];
374 8
        $pattern = '/^\*\s+remote\s+(.*)$/';
375 8
        preg_match($pattern, trim($line), $matches);
376 8
        if (!isset($matches[1])) {
377
            return '';
378
        }
379
380 8
        return $matches[1];
381
    }
382
383
    /**
384
     * get the matches from an output line
385
     *
386
     * @param string $remoteString remote line output
387
     *
388
     * @throws \InvalidArgumentException
389
     * @return array
390
     */
391 3
    public static function getMatches($remoteString)
392
    {
393 3
        $matches = [];
394 3
        preg_match('/^(\S+)\s*(\S[^\( ]+)\s*\((.+)\)$/', trim($remoteString), $matches);
395 3
        if (!count($matches)) {
396
            throw new \InvalidArgumentException(sprintf('the remote string is not valid: %s', $remoteString));
397
        }
398
399 3
        return array_map('trim', $matches);
400
    }
401
402
    /**
403
     * toString magic method
404
     *
405
     * @return string the named remote
406
     */
407 1
    public function __toString()
408
    {
409 1
        return $this->getName();
410
    }
411
412
    /**
413
     * name setter
414
     *
415
     * @param string $name the remote name
416
     */
417 1
    public function setName($name)
418
    {
419 1
        $this->name = $name;
420 1
    }
421
422
    /**
423
     * name getter
424
     *
425
     * @return string
426
     */
427 4
    public function getName()
428
    {
429 4
        return $this->name;
430
    }
431
432
    /**
433
     * fetchURL getter
434
     *
435
     * @return string
436
     */
437 2
    public function getFetchURL()
438
    {
439 2
        return $this->fetchURL;
440
    }
441
442
    /**
443
     * fetchURL setter
444
     *
445
     * @param string $url the fetch url
446
     */
447 1
    public function setFetchURL($url)
448
    {
449 1
        $this->fetchURL = $url;
450 1
    }
451
452
    /**
453
     * pushURL getter
454
     *
455
     * @return string
456
     */
457 2
    public function getPushURL()
458
    {
459 2
        return $this->pushURL;
460
    }
461
462
    /**
463
     * pushURL setter
464
     *
465
     * @param string $url the push url
466
     */
467 1
    public function setPushURL($url)
468
    {
469 1
        $this->pushURL = $url;
470 1
    }
471
472
    /**
473
     * remote HEAD branch getter
474
     *
475
     * @return string
476
     */
477 2
    public function getRemoteHEAD()
478
    {
479 2
        return $this->remoteHEAD;
480
    }
481
482
    /**
483
     * remote HEAD branch setter
484
     *
485
     * @param string $branchName
486
     */
487 1
    public function setRemoteHEAD($branchName)
488
    {
489 1
        $this->remoteHEAD = $branchName;
490 1
    }
491
492
    /**
493
     * get structured representation of branches
494
     *
495
     * @return array
496
     */
497 1
    public function getBranches()
498
    {
499 1
        return $this->branches;
500
    }
501
502
    /**
503
     * set structured representation of branches
504
     *
505
     * @param array $branches
506
     */
507 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...
508
    {
509 8
        $this->branches = $branches;
510 8
    }
511
}
512