Completed
Push — develop ( 5f7df1...9cb564 )
by Matteo
10s
created

Tree   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 500
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 61.18%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
dl 0
loc 500
wmc 56
c 4
b 0
f 0
lcom 1
cbo 7
ccs 93
cts 152
cp 0.6118
rs 6.5957

31 Methods

Rating   Name   Duplication   Size   Complexity  
A getCaller() 0 4 1
A isRoot() 0 4 1
A isBlob() 0 4 1
A isBinary() 0 4 2
A getLastCommitMessage() 0 4 1
A getLastCommitAuthor() 0 4 1
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 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
A createFromOutputLines() 0 7 1
A __construct() 0 8 1
A parseOutputLines() 0 8 2
A getParent() 0 8 2
A getBinaryData() 0 6 1
B getBreadcrumb() 0 21 5
B scanPathsForBlob() 0 17 5
B sortChildren() 0 11 5
C parseLine() 0 42 7
A getLastCommit() 0 9 2
A getObject() 0 8 2
A offsetSet() 0 8 2
A createFromCommand() 0 6 1

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
 * 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\Repository;
23
use \GitElephant\Command\LsTreeCommand;
24
use \GitElephant\Command\CatFileCommand;
25
26
/**
27
 * An abstraction of a git tree
28
 *
29
 * Retrieve an object with array access, iterable and countable
30
 * with a collection of Object at the given path of the repository
31
 *
32
 * @author Matteo Giachino <[email protected]>
33
 */
34
class Tree extends Object implements \ArrayAccess, \Countable, \Iterator
35
{
36
    /**
37
     * @var string
38
     */
39
    private $ref;
40
41
    /**
42
     * the cursor position
43
     *
44
     * @var int
45
     */
46
    private $position;
47
48
    /**
49
     * the tree subject
50
     *
51
     * @var Object
52
     */
53
    private $subject;
54
55
    /**
56
     * tree children
57
     *
58
     * @var array
59
     */
60
    private $children = array();
61
62
    /**
63
     * tree path children
64
     *
65
     * @var array
66
     */
67
    private $pathChildren = array();
68
69
    /**
70
     * the blob of the actual tree
71
     *
72
     * @var \GitElephant\Objects\Object
73
     */
74
    private $blob;
75
76
    /**
77
     * static method to generate standalone log
78
     *
79
     * @param \GitElephant\Repository $repository  repo
80
     * @param array                   $outputLines output lines from command.log
81
     *
82
     * @return \GitElephant\Objects\Log
83
     */
84
    public static function createFromOutputLines(Repository $repository, $outputLines)
85
    {
86
        $tree = new self($repository);
87
        $tree->parseOutputLines($outputLines);
88
89
        return $tree;
90
    }
91
92
    /**
93
     * get the commit properties from command
94
     *
95
     * @see LsTreeCommand::tree
96
     */
97 15
    private function createFromCommand()
98
    {
99 15
        $command = LsTreeCommand::getInstance($this->getRepository())->tree($this->ref, $this->subject);
100 15
        $outputLines = $this->getCaller()->execute($command)->getOutputLines(true);
101 15
        $this->parseOutputLines($outputLines);
102 15
    }
103
104
    /**
105
     * Some path examples:
106
     *    empty string for root
107
     *    folder1/folder2
108
     *    folder1/folder2/filename
109
     *
110
     * @param \GitElephant\Repository $repository the repository
111
     * @param string                  $ref        a treeish reference
112
     * @param Object                  $subject    the subject
113
     *
114
     * @throws \RuntimeException
115
     * @throws \Symfony\Component\Process\Exception\RuntimeException
116
     * @internal param \GitElephant\Objects\Object|string $treeObject Object instance
117
     */
118 15
    public function __construct(Repository $repository, $ref = 'HEAD', $subject = null)
119
    {
120 15
        $this->position   = 0;
121 15
        $this->repository = $repository;
122 15
        $this->ref = $ref;
123 15
        $this->subject = $subject;
124 15
        $this->createFromCommand();
125 15
    }
126
127
    /**
128
     * parse the output of a git command showing a ls-tree
129
     *
130
     * @param array $outputLines output lines
131
     */
132 15
    private function parseOutputLines($outputLines)
133
    {
134 15
        foreach ($outputLines as $line) {
135 15
            $this->parseLine($line);
136 15
        }
137 15
        usort($this->children, array($this, 'sortChildren'));
138 15
        $this->scanPathsForBlob($outputLines);
139 15
    }
140
141
    /**
142
     * @return \GitElephant\Command\Caller\Caller
143
     */
144 15
    private function getCaller()
145
    {
146 15
        return $this->getRepository()->getCaller();
147
    }
148
149
    /**
150
     * get the current tree parent, null if root
151
     *
152
     * @return null|string
153
     */
154
    public function getParent()
155
    {
156
        if ($this->isRoot()) {
157
            return null;
158
        }
159
160
        return substr($this->subject->getFullPath(), 0, strrpos($this->subject->getFullPath(), '/'));
161
    }
162
163
    /**
164
     * tell if the tree created is the root of the repository
165
     *
166
     * @return bool
167
     */
168 15
    public function isRoot()
169
    {
170 15
        return null === $this->subject;
171
    }
172
173
    /**
174
     * tell if the path given is a blob path
175
     *
176
     * @return bool
177
     */
178 15
    public function isBlob()
179
    {
180 15
        return isset($this->blob);
181
    }
182
183
    /**
184
     * the current tree path is a binary file
185
     *
186
     * @return bool
187
     */
188
    public function isBinary()
189
    {
190
        return $this->isRoot() ? false : Object::TYPE_BLOB === $this->subject->getType();
191
    }
192
193
    /**
194
     * get binary data
195
     *
196
     * @throws \RuntimeException
197
     * @throws \Symfony\Component\Process\Exception\LogicException
198
     * @throws \Symfony\Component\Process\Exception\InvalidArgumentException
199
     * @throws \Symfony\Component\Process\Exception\RuntimeException
200
     * @return string
201
     */
202
    public function getBinaryData()
203
    {
204
        $cmd = CatFileCommand::getInstance($this->getRepository())->content($this->getSubject(), $this->ref);
205
206
        return $this->getCaller()->execute($cmd)->getRawOutput();
207
    }
208
209
    /**
210
     * Return an array like this
211
     *   0 => array(
212
     *      'path' => the path to the current element
213
     *      'label' => the name of the current element
214
     *   ),
215
     *   1 => array(),
216
     *   ...
217
     *
218
     * @return array
219
     */
220
    public function getBreadcrumb()
221
    {
222
        $bc = array();
223
        if (!$this->isRoot()) {
224
            $arrayNames = explode('/', $this->subject->getFullPath());
225
            $pathString = '';
226
            foreach ($arrayNames as $i => $name) {
227
                if ($this->isBlob() && $name == $this->blob->getName()) {
228
                    $bc[$i]['path']  = $pathString . $name;
229
                    $bc[$i]['label'] = $this->blob;
230
                    $pathString .= $name . '/';
231
                } else {
232
                    $bc[$i]['path']  = $pathString . $name;
233
                    $bc[$i]['label'] = $name;
234
                    $pathString .= $name . '/';
235
                }
236
            }
237
        }
238
239
        return $bc;
240
    }
241
242
    /**
243
     * check if the path is equals to a fullPath
244
     * to tell if it's a blob
245
     *
246
     * @param array $outputLines output lines
247
     *
248
     * @return mixed
249
     */
250 15
    private function scanPathsForBlob($outputLines)
251
    {
252
        // no children, empty folder or blob!
253 15
        if (count($this->children) > 0) {
254 10
            return;
255
        }
256
        // root, no blob
257 7
        if ($this->isRoot()) {
258
            return;
259
        }
260 7
        if (1 === count($outputLines)) {
261 7
            $treeObject = Object::createFromOutputLine($this->repository, $outputLines[0]);
262 7
            if ($treeObject->getSha() === $this->subject->getSha()) {
263 7
                $this->blob = $treeObject;
264 7
            }
265 7
        }
266 7
    }
267
268
    /**
269
     * Reorder children of the tree
270
     * Tree first (alphabetically) and then blobs (alphabetically)
271
     *
272
     * @param \GitElephant\Objects\Object $a the first object
273
     * @param \GitElephant\Objects\Object $b the second object
274
     *
275
     * @return int
276
     */
277 7
    private function sortChildren(Object $a, Object $b)
278
    {
279 7
        if ($a->getType() == $b->getType()) {
280 5
            $names = array($a->getName(), $b->getName());
281 5
            sort($names);
282
283 5
            return ($a->getName() == $names[0]) ? -1 : 1;
284
        }
285
286 5
        return $a->getType() == Object::TYPE_TREE || $b->getType() == Object::TYPE_BLOB ? -1 : 1;
287
    }
288
289
    /**
290
     * Parse a single line into pieces
291
     *
292
     * @param string $line a single line output from the git binary
293
     *
294
     * @return mixed
295
     */
296 15
    private function parseLine($line)
297
    {
298 15
        if ($line == '') {
299
            return;
300
        }
301 15
        $slices = Object::getLineSlices($line);
302 15
        if ($this->isBlob()) {
303
            $this->pathChildren[] = $this->blob->getName();
304
        } else {
305 15
            if ($this->isRoot()) {
306
                // if is root check for first children
307 9
                $pattern     = '/(\w+)\/(.*)/';
308 9
                $replacement = '$1';
309 9
            } else {
310
                // filter by the children of the path
311 10
                $actualPath = $this->subject->getFullPath();
312 10
                if (!preg_match(sprintf('/^%s\/(\w*)/', preg_quote($actualPath, '/')), $slices['fullPath'])) {
313 7
                    return;
314
                }
315 5
                $pattern     = sprintf('/^%s\/(\w*)/', preg_quote($actualPath, '/'));
316 5
                $replacement = '$1';
317
            }
318 10
            $name = preg_replace($pattern, $replacement, $slices['fullPath']);
319 10
            if (strpos($name, '/') !== false) {
320
                return;
321
            }
322 10
            if (!in_array($name, $this->pathChildren)) {
323 10
                $path                 = rtrim(rtrim($slices['fullPath'], $name), '/');
324 10
                $treeObject           = new TreeObject(
325 10
                    $this->repository,
326 10
                    $slices['permissions'],
327 10
                    $slices['type'],
328 10
                    $slices['sha'],
329 10
                    $slices['size'],
330 10
                    $name,
331
                    $path
332 10
                );
333 10
                $this->children[]     = $treeObject;
334 10
                $this->pathChildren[] = $name;
335 10
            }
336
        }
337 10
    }
338
339
    /**
340
     * get the last commit message for this tree
341
     *
342
     * @param string $ref
343
     *
344
     * @throws \RuntimeException
345
     * @return Commit\Message
346
     */
347
    public function getLastCommitMessage($ref = 'master')
348
    {
349
        return $this->getLastCommit($ref)->getMessage();
350
    }
351
352
    /**
353
     * get author of the last commit
354
     *
355
     * @param string $ref
356
     *
357
     * @throws \RuntimeException
358
     * @return Author
359
     */
360
    public function getLastCommitAuthor($ref = 'master')
361
    {
362
        return $this->getLastCommit($ref)->getAuthor();
363
    }
364
365
    /**
366
     * get the last commit for a given treeish, for the actual tree
367
     *
368
     * @param string $ref
369
     *
370
     * @throws \RuntimeException
371
     * @throws \Symfony\Component\Process\Exception\RuntimeException
372
     * @return Commit
373
     */
374
    public function getLastCommit($ref = 'master')
375
    {
376
        if ($this->isRoot()) {
377
            return $this->getRepository()->getCommit($ref);
378
        }
379
        $log = $this->repository->getObjectLog($this->getObject(), $ref);
0 ignored issues
show
Bug introduced by
It seems like $this->getObject() can be null; however, getObjectLog() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
380
381
        return $log[0];
382
    }
383
384
    /**
385
     * get the tree object for this tree
386
     *
387
     * @return \GitElephant\Objects\Object
388
     */
389 1
    public function getObject()
390
    {
391 1
        if ($this->isRoot()) {
392 1
            return null;
393
        } else {
394 1
            return $this->getSubject();
395
        }
396
    }
397
398
    /**
399
     * Blob getter
400
     *
401
     * @return \GitElephant\Objects\Object
402
     */
403 5
    public function getBlob()
404
    {
405 5
        return $this->blob;
406
    }
407
408
    /**
409
     * Get Subject
410
     *
411
     * @return \GitElephant\Objects\Object
412
     */
413 1
    public function getSubject()
414
    {
415 1
        return $this->subject;
416
    }
417
418
    /**
419
     * Get Ref
420
     *
421
     * @return string
422
     */
423
    public function getRef()
424
    {
425
        return $this->ref;
426
    }
427
428
    /**
429
     * ArrayAccess interface
430
     *
431
     * @param int $offset offset
432
     *
433
     * @return bool
434
     */
435
    public function offsetExists($offset)
436
    {
437
        return isset($this->children[$offset]);
438
    }
439
440
441
    /**
442
     * ArrayAccess interface
443
     *
444
     * @param int $offset offset
445
     *
446
     * @return null
447
     */
448 8
    public function offsetGet($offset)
449
    {
450 8
        return isset($this->children[$offset]) ? $this->children[$offset] : null;
451
    }
452
453
    /**
454
     * ArrayAccess interface
455
     *
456
     * @param int   $offset offset
457
     * @param mixed $value  value
458
     */
459
    public function offsetSet($offset, $value)
460
    {
461
        if (is_null($offset)) {
462
            $this->children[] = $value;
463
        } else {
464
            $this->children[$offset] = $value;
465
        }
466
    }
467
468
    /**
469
     * ArrayAccess interface
470
     *
471
     * @param int $offset offset
472
     */
473
    public function offsetUnset($offset)
474
    {
475
        unset($this->children[$offset]);
476
    }
477
478
    /**
479
     * Countable interface
480
     *
481
     * @return int|void
482
     */
483 4
    public function count()
484
    {
485 4
        return count($this->children);
486
    }
487
488
    /**
489
     * Iterator interface
490
     *
491
     * @return mixed
492
     */
493 1
    public function current()
494
    {
495 1
        return $this->children[$this->position];
496
    }
497
498
    /**
499
     * Iterator interface
500
     */
501 1
    public function next()
502
    {
503 1
        ++$this->position;
504 1
    }
505
506
    /**
507
     * Iterator interface
508
     *
509
     * @return int
510
     */
511
    public function key()
512
    {
513
        return $this->position;
514
    }
515
516
    /**
517
     * Iterator interface
518
     *
519
     * @return bool
520
     */
521 1
    public function valid()
522
    {
523 1
        return isset($this->children[$this->position]);
524
    }
525
526
    /**
527
     * Iterator interface
528
     */
529 1
    public function rewind()
530
    {
531 1
        $this->position = 0;
532 1
    }
533
}
534