Completed
Pull Request — develop (#168)
by
unknown
01:45
created

Tree   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 503
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 61.7%

Importance

Changes 0
Metric Value
wmc 56
lcom 1
cbo 7
dl 0
loc 503
ccs 87
cts 141
cp 0.617
rs 5.5199
c 0
b 0
f 0

31 Methods

Rating   Name   Duplication   Size   Complexity  
A createFromOutputLines() 0 7 1
A createFromCommand() 0 6 1
A __construct() 0 8 1
A parseOutputLines() 0 10 2
A getCaller() 0 4 1
A getParent() 0 8 2
A isRoot() 0 4 1
A isBlob() 0 4 1
A isBinary() 0 4 2
A getBinaryData() 0 6 1
A getBreadcrumb() 0 21 5
A scanPathsForBlob() 0 19 5
A sortChildren() 0 11 5
A getLastCommitMessage() 0 4 1
A getLastCommitAuthor() 0 4 1
A getLastCommit() 0 9 2
A getObject() 0 4 2
A getBlob() 0 4 1
A getSubject() 0 4 1
A getRef() 0 4 1
A offsetExists() 0 4 1
A offsetGet() 0 4 2
A offsetSet() 0 8 2
A offsetUnset() 0 4 1
A count() 0 4 1
A current() 0 4 1
A next() 0 4 1
A key() 0 4 1
A valid() 0 4 1
A rewind() 0 4 1
B parseLine() 0 45 7

How to fix   Complexity   

Complex Class

Complex classes like Tree 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 Tree, 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\Caller\CallerInterface;
24
use GitElephant\Command\CatFileCommand;
25
use GitElephant\Command\LsTreeCommand;
26
use GitElephant\Repository;
27
28
/**
29
 * An abstraction of a git tree
30
 *
31
 * Retrieve an object with array access, iterable and countable
32
 * with a collection of Object at the given path of the repository
33
 *
34
 * @author Matteo Giachino <[email protected]>
35
 */
36
class Tree extends NodeObject implements \ArrayAccess, \Countable, \Iterator
37
{
38
    /**
39
     * @var string
40
     */
41
    private $ref;
42
43
    /**
44
     * the cursor position
45
     *
46
     * @var int
47
     */
48
    private $position;
49
50
    /**
51
     * the tree subject
52
     *
53
     * @var NodeObject
54
     */
55
    private $subject;
56
57
    /**
58
     * tree children
59
     *
60
     * @var array
61
     */
62
    private $children = [];
63
64
    /**
65
     * tree path children
66
     *
67
     * @var array
68
     */
69
    private $pathChildren = [];
70
71
    /**
72
     * the blob of the actual tree
73
     *
74
     * @var \GitElephant\Objects\NodeObject
75
     */
76
    private $blob;
77
78
    /**
79
     * static method to generate standalone log
80
     *
81
     * @param \GitElephant\Repository $repository  repo
82
     * @param array                   $outputLines output lines from command.log
83
     *
84
     * @return \GitElephant\Objects\Tree
85
     */
86
    public static function createFromOutputLines(Repository $repository, array $outputLines): \GitElephant\Objects\Tree
87
    {
88
        $tree = new self($repository);
89
        $tree->parseOutputLines($outputLines);
90
91
        return $tree;
92
    }
93
94
    /**
95
     * get the commit properties from command
96
     *
97
     * @see LsTreeCommand::tree
98
     */
99 15
    private function createFromCommand(): void
100
    {
101 15
        $command = LsTreeCommand::getInstance($this->getRepository())->tree($this->ref, $this->subject);
102 15
        $outputLines = $this->getCaller()->execute($command)->getOutputLines(true);
103 15
        $this->parseOutputLines($outputLines);
104 15
    }
105
106
    /**
107
     * Some path examples:
108
     *    empty string for root
109
     *    folder1/folder2
110
     *    folder1/folder2/filename
111
     *
112
     * @param \GitElephant\Repository $repository the repository
113
     * @param string                  $ref        a treeish reference
114
     * @param NodeObject              $subject    the subject
115
     *
116
     * @throws \RuntimeException
117
     * @throws \Symfony\Component\Process\Exception\RuntimeException
118
     * @internal param \GitElephant\Objects\Object|string $treeObject Object instance
119
     */
120 15
    public function __construct(Repository $repository, $ref = 'HEAD', NodeObject $subject = null)
121
    {
122 15
        $this->position = 0;
123 15
        $this->repository = $repository;
124 15
        $this->ref = $ref;
125 15
        $this->subject = $subject;
126 15
        $this->createFromCommand();
127 15
    }
128
129
    /**
130
     * parse the output of a git command showing a ls-tree
131
     *
132
     * @param array $outputLines output lines
133
     */
134 15
    private function parseOutputLines(array $outputLines): void
135
    {
136 15
        foreach ($outputLines as $line) {
137 15
            $this->parseLine($line);
138
        }
139
        usort($this->children, function ($a, $b) {
140 7
            return self::sortChildren($a, $b);
141 15
        });
142 15
        $this->scanPathsForBlob($outputLines);
143 15
    }
144
145
    /**
146
     * @return CallerInterface
147
     */
148 15
    private function getCaller(): CallerInterface
149
    {
150 15
        return $this->getRepository()->getCaller();
151
    }
152
153
    /**
154
     * get the current tree parent, null if root
155
     *
156
     * @return null|string
157
     */
158
    public function getParent(): ?string
159
    {
160
        if ($this->isRoot()) {
161
            return null;
162
        }
163
164
        return substr($this->subject->getFullPath(), 0, strrpos($this->subject->getFullPath(), '/'));
165
    }
166
167
    /**
168
     * tell if the tree created is the root of the repository
169
     *
170
     * @return bool
171
     */
172 15
    public function isRoot(): bool
173
    {
174 15
        return null === $this->subject;
175
    }
176
177
    /**
178
     * tell if the path given is a blob path
179
     *
180
     * @return bool
181
     */
182 15
    public function isBlob(): bool
183
    {
184 15
        return isset($this->blob);
185
    }
186
187
    /**
188
     * the current tree path is a binary file
189
     *
190
     * @return bool
191
     */
192
    public function isBinary(): bool
193
    {
194
        return $this->isRoot() ? false : NodeObject::TYPE_BLOB === $this->subject->getType();
195
    }
196
197
    /**
198
     * get binary data
199
     *
200
     * @throws \RuntimeException
201
     * @throws \Symfony\Component\Process\Exception\LogicException
202
     * @throws \Symfony\Component\Process\Exception\InvalidArgumentException
203
     * @throws \Symfony\Component\Process\Exception\RuntimeException
204
     * @return string
205
     */
206
    public function getBinaryData(): string
207
    {
208
        $cmd = CatFileCommand::getInstance($this->getRepository())->content($this->getSubject(), $this->ref);
209
210
        return $this->getCaller()->execute($cmd)->getRawOutput();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GitElephant\Command\Caller\CallerInterface as the method getRawOutput() does only exist in the following implementations of said interface: GitElephant\Command\Caller\Caller.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
211
    }
212
213
    /**
214
     * Return an array like this
215
     *   0 => array(
216
     *      'path' => the path to the current element
217
     *      'label' => the name of the current element
218
     *   ),
219
     *   1 => array(),
220
     *   ...
221
     *
222
     * @return array
223
     */
224
    public function getBreadcrumb(): array
225
    {
226
        $bc = [];
227
        if (!$this->isRoot()) {
228
            $arrayNames = explode('/', $this->subject->getFullPath());
229
            $pathString = '';
230
            foreach ($arrayNames as $i => $name) {
231
                if ($this->isBlob() and $name === $this->blob->getName()) {
232
                    $bc[$i]['path'] = $pathString . $name;
233
                    $bc[$i]['label'] = $this->blob;
234
                    $pathString .= $name . '/';
235
                } else {
236
                    $bc[$i]['path'] = $pathString . $name;
237
                    $bc[$i]['label'] = $name;
238
                    $pathString .= $name . '/';
239
                }
240
            }
241
        }
242
243
        return $bc;
244
    }
245
246
    /**
247
     * check if the path is equals to a fullPath
248
     * to tell if it's a blob
249
     *
250
     * @param array $outputLines output lines
251
     *
252
     * @return mixed
253
     */
254 15
    private function scanPathsForBlob(array $outputLines): void
255
    {
256
        // no children, empty folder or blob!
257 15
        if (count($this->children) > 0) {
258 10
            return;
259
        }
260
261
        // root, no blob
262 7
        if ($this->isRoot()) {
263
            return;
264
        }
265
266 7
        if (1 === count($outputLines)) {
267 7
            $treeObject = NodeObject::createFromOutputLine($this->repository, $outputLines[0]);
268 7
            if ($treeObject->getSha() === $this->subject->getSha()) {
269 7
                $this->blob = $treeObject;
270
            }
271
        }
272 7
    }
273
274
    /**
275
     * Reorder children of the tree
276
     * Tree first (alphabetically) and then blobs (alphabetically)
277
     *
278
     * @param \GitElephant\Objects\NodeObject $a the first object
279
     * @param \GitElephant\Objects\NodeObject $b the second object
280
     *
281
     * @return int
282
     */
283 7
    private static function sortChildren(NodeObject $a, NodeObject $b): int
284
    {
285 7
        if ($a->getType() === $b->getType()) {
286 5
            $names = [$a->getName(), $b->getName()];
287 5
            sort($names);
288
289 5
            return $a->getName() === $names[0] ? -1 : 1;
290
        }
291
292 5
        return $a->getType() === NodeObject::TYPE_TREE || $b->getType() === NodeObject::TYPE_BLOB ? -1 : 1;
293
    }
294
295
    /**
296
     * Parse a single line into pieces
297
     *
298
     * @param string $line a single line output from the git binary
299
     *
300
     * @return mixed
301
     */
302 15
    private function parseLine(string $line): void
303
    {
304 15
        if ($line === '') {
305
            return;
306
        }
307
308 15
        $slices = NodeObject::getLineSlices($line);
309 15
        if ($this->isBlob()) {
310
            $this->pathChildren[] = $this->blob->getName();
311
        } else {
312 15
            if ($this->isRoot()) {
313
                // if is root check for first children
314 9
                $pattern = '/(\w+)\/(.*)/';
315 9
                $replacement = '$1';
316
            } else {
317
                // filter by the children of the path
318 10
                $actualPath = $this->subject->getFullPath();
319 10
                if (!preg_match(sprintf('/^%s\/(\w*)/', preg_quote($actualPath, '/')), $slices['fullPath'])) {
320 7
                    return;
321
                }
322 5
                $pattern = sprintf('/^%s\/(\w*)/', preg_quote($actualPath, '/'));
323 5
                $replacement = '$1';
324
            }
325
326 10
            $name = preg_replace($pattern, $replacement, $slices['fullPath']);
327 10
            if (strpos($name, '/') !== false) {
328
                return;
329
            }
330
331 10
            if (!in_array($name, $this->pathChildren)) {
332 10
                $path = rtrim(rtrim($slices['fullPath'], $name), '/');
333 10
                $treeObject = new TreeObject(
334 10
                    $this->repository,
335 10
                    $slices['permissions'],
336 10
                    $slices['type'],
337 10
                    $slices['sha'],
338 10
                    $slices['size'],
339 10
                    $name,
340 10
                    $path
341
                );
342 10
                $this->children[] = $treeObject;
343 10
                $this->pathChildren[] = $name;
344
            }
345
        }
346 10
    }
347
348
    /**
349
     * get the last commit message for this tree
350
     *
351
     * @param string $ref
352
     *
353
     * @throws \RuntimeException
354
     * @return Commit\Message
355
     */
356
    public function getLastCommitMessage($ref = 'master'): \GitElephant\Objects\Commit\Message
357
    {
358
        return $this->getLastCommit($ref)->getMessage();
359
    }
360
361
    /**
362
     * get author of the last commit
363
     *
364
     * @param string $ref
365
     *
366
     * @throws \RuntimeException
367
     * @return Author
368
     */
369
    public function getLastCommitAuthor($ref = 'master'): \GitElephant\Objects\Author
370
    {
371
        return $this->getLastCommit($ref)->getAuthor();
372
    }
373
374
    /**
375
     * get the last commit for a given treeish, for the actual tree
376
     *
377
     * @param string $ref
378
     *
379
     * @throws \RuntimeException
380
     * @throws \Symfony\Component\Process\Exception\RuntimeException
381
     * @return Commit
382
     */
383
    public function getLastCommit($ref = 'master'): ?\GitElephant\Objects\Commit
384
    {
385
        if ($this->isRoot()) {
386
            return $this->getRepository()->getCommit($ref);
387
        }
388
        $log = $this->repository->getObjectLog($this->getObject(), $ref);
389
390
        return $log[0];
391
    }
392
393
    /**
394
     * get the tree object for this tree
395
     *
396
     * @return \GitElephant\Objects\NodeObject
397
     */
398 1
    public function getObject(): ?\GitElephant\Objects\NodeObject
399
    {
400 1
        return $this->isRoot() ? null : $this->getSubject();
401
    }
402
403
    /**
404
     * Blob getter
405
     *
406
     * @return \GitElephant\Objects\NodeObject
407
     */
408 5
    public function getBlob(): \GitElephant\Objects\NodeObject
409
    {
410 5
        return $this->blob;
411
    }
412
413
    /**
414
     * Get Subject
415
     *
416
     * @return \GitElephant\Objects\NodeObject
417
     */
418 1
    public function getSubject(): \GitElephant\Objects\NodeObject
419
    {
420 1
        return $this->subject;
421
    }
422
423
    /**
424
     * Get Ref
425
     *
426
     * @return string
427
     */
428
    public function getRef(): string
429
    {
430
        return $this->ref;
431
    }
432
433
    /**
434
     * ArrayAccess interface
435
     *
436
     * @param int $offset offset
437
     *
438
     * @return bool
439
     */
440
    public function offsetExists($offset): bool
441
    {
442
        return isset($this->children[$offset]);
443
    }
444
445
446
    /**
447
     * ArrayAccess interface
448
     *
449
     * @param int $offset offset
450
     *
451
     * @return null
452
     */
453 8
    public function offsetGet($offset)
454
    {
455 8
        return isset($this->children[$offset]) ? $this->children[$offset] : null;
456
    }
457
458
    /**
459
     * ArrayAccess interface
460
     *
461
     * @param int   $offset offset
462
     * @param mixed $value  value
463
     */
464
    public function offsetSet($offset, $value): void
465
    {
466
        if (is_null($offset)) {
467
            $this->children[] = $value;
468
        } else {
469
            $this->children[$offset] = $value;
470
        }
471
    }
472
473
    /**
474
     * ArrayAccess interface
475
     *
476
     * @param int $offset offset
477
     */
478
    public function offsetUnset($offset): void
479
    {
480
        unset($this->children[$offset]);
481
    }
482
483
    /**
484
     * Countable interface
485
     *
486
     * @return int
487
     */
488 4
    public function count(): int
489
    {
490 4
        return count($this->children);
491
    }
492
493
    /**
494
     * Iterator interface
495
     *
496
     * @return mixed
497
     */
498 1
    public function current()
499
    {
500 1
        return $this->children[$this->position];
501
    }
502
503
    /**
504
     * Iterator interface
505
     */
506 1
    public function next(): void
507
    {
508 1
        ++$this->position;
509 1
    }
510
511
    /**
512
     * Iterator interface
513
     *
514
     * @return int
515
     */
516
    public function key(): int
517
    {
518
        return $this->position;
519
    }
520
521
    /**
522
     * Iterator interface
523
     *
524
     * @return bool
525
     */
526 1
    public function valid(): bool
527
    {
528 1
        return isset($this->children[$this->position]);
529
    }
530
531
    /**
532
     * Iterator interface
533
     */
534 1
    public function rewind(): void
535
    {
536 1
        $this->position = 0;
537 1
    }
538
}
539