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

Tree   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 505
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 61.43%

Importance

Changes 0
Metric Value
wmc 56
c 0
b 0
f 0
lcom 1
cbo 7
dl 0
loc 505
ccs 86
cts 140
cp 0.6143
rs 6.5957

31 Methods

Rating   Name   Duplication   Size   Complexity  
A createFromOutputLines() 0 7 1
A createFromCommand() 0 6 1
A __construct() 0 8 1
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 parseOutputLines() 0 8 2
B getBreadcrumb() 0 22 5
B scanPathsForBlob() 0 19 5
B sortChildren() 0 11 5
C parseLine() 0 47 7
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 9 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

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