Completed
Push — master ( c1a738...2c88a4 )
by
unknown
02:35 queued 01:13
created

Tree::parseLine()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 7.049

Importance

Changes 0
Metric Value
dl 0
loc 45
ccs 27
cts 30
cp 0.9
rs 8.2666
c 0
b 0
f 0
cc 7
nc 9
nop 1
crap 7.049
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|null
40
     */
41
    private $ref;
42
43
    /**
44
     * the cursor position
45
     *
46
     * @var int|null
47
     */
48
    private $position;
49
50
    /**
51
     * the tree subject
52
     *
53
     * @var NodeObject|null
54
     */
55
    private $subject;
56
57
    /**
58
     * tree children
59
     *
60
     * @var array<TreeObject>
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|null
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);
0 ignored issues
show
Bug introduced by
It seems like $this->getSubject() can be null; however, content() 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...
209
210
        return $this->getCaller()->execute($cmd)->getRawOutput();
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 void
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 void
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);
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...
389
390
        return $log[0];
391
    }
392
393
    /**
394
     * get the tree object for this tree
395
     *
396
     * @return \GitElephant\Objects\NodeObject|null
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|null
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|null
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|null
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 NodeObject|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|null   $offset offset
462
     * @param TreeObject $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 TreeObject|null
497
     */
498 1
    public function current(): ?TreeObject
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