Completed
Pull Request — develop (#165)
by
unknown
01:48
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\Repository;
24
use \GitElephant\Command\LsTreeCommand;
25
use \GitElephant\Command\CatFileCommand;
26
use \GitElephant\Command\Caller\CallerInterface;
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
        return $tree;
91
    }
92
93
    /**
94
     * get the commit properties from command
95
     *
96
     * @see LsTreeCommand::tree
97
     */
98 15
    private function createFromCommand(): void
99
    {
100 15
        $command = LsTreeCommand::getInstance($this->getRepository())->tree($this->ref, $this->subject);
101 15
        $outputLines = $this->getCaller()->execute($command)->getOutputLines(true);
102 15
        $this->parseOutputLines($outputLines);
103 15
    }
104
105
    /**
106
     * Some path examples:
107
     *    empty string for root
108
     *    folder1/folder2
109
     *    folder1/folder2/filename
110
     *
111
     * @param \GitElephant\Repository $repository the repository
112
     * @param string                  $ref        a treeish reference
113
     * @param NodeObject              $subject    the subject
114
     *
115
     * @throws \RuntimeException
116
     * @throws \Symfony\Component\Process\Exception\RuntimeException
117
     * @internal param \GitElephant\Objects\Object|string $treeObject Object instance
118
     */
119 15
    public function __construct(Repository $repository, $ref = 'HEAD', NodeObject $subject = null)
120
    {
121 15
        $this->position = 0;
122 15
        $this->repository = $repository;
123 15
        $this->ref = $ref;
124 15
        $this->subject = $subject;
125 15
        $this->createFromCommand();
126 15
    }
127
128
    /**
129
     * parse the output of a git command showing a ls-tree
130
     *
131
     * @param array $outputLines output lines
132
     */
133 15
    private function parseOutputLines(array $outputLines): void
134
    {
135 15
        foreach ($outputLines as $line) {
136 15
            $this->parseLine($line);
137
        }
138 15
        usort($this->children, [$this, 'sortChildren']);
139 15
        $this->scanPathsForBlob($outputLines);
140 15
    }
141
142
    /**
143
     * @return CallerInterface
144
     */
145 15
    private function getCaller(): CallerInterface
146
    {
147 15
        return $this->getRepository()->getCaller();
148
    }
149
150
    /**
151
     * get the current tree parent, null if root
152
     *
153
     * @return null|string
154
     */
155
    public function getParent(): ?string
156
    {
157
        if ($this->isRoot()) {
158
            return null;
159
        }
160
161
        return substr($this->subject->getFullPath(), 0, strrpos($this->subject->getFullPath(), '/'));
162
    }
163
164
    /**
165
     * tell if the tree created is the root of the repository
166
     *
167
     * @return bool
168
     */
169 15
    public function isRoot(): bool
170
    {
171 15
        return null === $this->subject;
172
    }
173
174
    /**
175
     * tell if the path given is a blob path
176
     *
177
     * @return bool
178
     */
179 15
    public function isBlob(): bool
180
    {
181 15
        return isset($this->blob);
182
    }
183
184
    /**
185
     * the current tree path is a binary file
186
     *
187
     * @return bool
188
     */
189
    public function isBinary(): bool
190
    {
191
        return $this->isRoot() ? false : NodeObject::TYPE_BLOB === $this->subject->getType();
192
    }
193
194
    /**
195
     * get binary data
196
     *
197
     * @throws \RuntimeException
198
     * @throws \Symfony\Component\Process\Exception\LogicException
199
     * @throws \Symfony\Component\Process\Exception\InvalidArgumentException
200
     * @throws \Symfony\Component\Process\Exception\RuntimeException
201
     * @return string
202
     */
203
    public function getBinaryData(): string
204
    {
205
        $cmd = CatFileCommand::getInstance($this->getRepository())->content($this->getSubject(), $this->ref);
206
207
        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...
208
    }
209
210
    /**
211
     * Return an array like this
212
     *   0 => array(
213
     *      'path' => the path to the current element
214
     *      'label' => the name of the current element
215
     *   ),
216
     *   1 => array(),
217
     *   ...
218
     *
219
     * @return array
220
     */
221
    public function getBreadcrumb(): array
222
    {
223
        $bc = [];
224
        if (!$this->isRoot()) {
225
            $arrayNames = explode('/', $this->subject->getFullPath());
226
            $pathString = '';
227
            foreach ($arrayNames as $i => $name) {
228
                if ($this->isBlob() and $name === $this->blob->getName()) {
229
                    $bc[$i]['path'] = $pathString . $name;
230
                    $bc[$i]['label'] = $this->blob;
231
                    $pathString .= $name . '/';
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): void
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): int
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): void
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
        } else {
309 15
            if ($this->isRoot()) {
310
                // if is root check for first children
311 9
                $pattern = '/(\w+)\/(.*)/';
312 9
                $replacement = '$1';
313
            } else {
314
                // filter by the children of the path
315 10
                $actualPath = $this->subject->getFullPath();
316 10
                if (!preg_match(sprintf('/^%s\/(\w*)/', preg_quote($actualPath, '/')), $slices['fullPath'])) {
317 7
                    return;
318
                }
319 5
                $pattern = sprintf('/^%s\/(\w*)/', preg_quote($actualPath, '/'));
320 5
                $replacement = '$1';
321
            }
322
323 10
            $name = preg_replace($pattern, $replacement, $slices['fullPath']);
324 10
            if (strpos($name, '/') !== false) {
325
                return;
326
            }
327
328 10
            if (!in_array($name, $this->pathChildren)) {
329 10
                $path = rtrim(rtrim($slices['fullPath'], $name), '/');
330 10
                $treeObject = new TreeObject(
331 10
                    $this->repository,
332 10
                    $slices['permissions'],
333 10
                    $slices['type'],
334 10
                    $slices['sha'],
335 10
                    $slices['size'],
336 10
                    $name,
337 10
                    $path
338
                );
339 10
                $this->children[] = $treeObject;
340 10
                $this->pathChildren[] = $name;
341
            }
342
        }
343 10
    }
344
345
    /**
346
     * get the last commit message for this tree
347
     *
348
     * @param string $ref
349
     *
350
     * @throws \RuntimeException
351
     * @return Commit\Message
352
     */
353
    public function getLastCommitMessage($ref = 'master'): \GitElephant\Objects\Commit\Message
354
    {
355
        return $this->getLastCommit($ref)->getMessage();
356
    }
357
358
    /**
359
     * get author of the last commit
360
     *
361
     * @param string $ref
362
     *
363
     * @throws \RuntimeException
364
     * @return Author
365
     */
366
    public function getLastCommitAuthor($ref = 'master'): \GitElephant\Objects\Author
367
    {
368
        return $this->getLastCommit($ref)->getAuthor();
369
    }
370
371
    /**
372
     * get the last commit for a given treeish, for the actual tree
373
     *
374
     * @param string $ref
375
     *
376
     * @throws \RuntimeException
377
     * @throws \Symfony\Component\Process\Exception\RuntimeException
378
     * @return Commit
379
     */
380
    public function getLastCommit($ref = 'master'): ?\GitElephant\Objects\Commit
381
    {
382
        if ($this->isRoot()) {
383
            return $this->getRepository()->getCommit($ref);
384
        }
385
        $log = $this->repository->getObjectLog($this->getObject(), $ref);
386
        return $log[0];
387
    }
388
389
    /**
390
     * get the tree object for this tree
391
     *
392
     * @return \GitElephant\Objects\NodeObject
393
     */
394 1
    public function getObject(): ?\GitElephant\Objects\NodeObject
395
    {
396 1
        return $this->isRoot() ? null : $this->getSubject();
397
    }
398
399
    /**
400
     * Blob getter
401
     *
402
     * @return \GitElephant\Objects\NodeObject
403
     */
404 5
    public function getBlob(): \GitElephant\Objects\NodeObject
405
    {
406 5
        return $this->blob;
407
    }
408
409
    /**
410
     * Get Subject
411
     *
412
     * @return \GitElephant\Objects\NodeObject
413
     */
414 1
    public function getSubject(): \GitElephant\Objects\NodeObject
415
    {
416 1
        return $this->subject;
417
    }
418
419
    /**
420
     * Get Ref
421
     *
422
     * @return string
423
     */
424
    public function getRef(): string
425
    {
426
        return $this->ref;
427
    }
428
429
    /**
430
     * ArrayAccess interface
431
     *
432
     * @param int $offset offset
433
     *
434
     * @return bool
435
     */
436
    public function offsetExists($offset): bool
437
    {
438
        return isset($this->children[$offset]);
439
    }
440
441
442
    /**
443
     * ArrayAccess interface
444
     *
445
     * @param int $offset offset
446
     *
447
     * @return null
448
     */
449 8
    public function offsetGet($offset)
450
    {
451 8
        return isset($this->children[$offset]) ? $this->children[$offset] : null;
452
    }
453
454
    /**
455
     * ArrayAccess interface
456
     *
457
     * @param int   $offset offset
458
     * @param mixed $value  value
459
     */
460
    public function offsetSet($offset, $value): void
461
    {
462
        if (is_null($offset)) {
463
            $this->children[] = $value;
464
        } else {
465
            $this->children[$offset] = $value;
466
        }
467
    }
468
469
    /**
470
     * ArrayAccess interface
471
     *
472
     * @param int $offset offset
473
     */
474
    public function offsetUnset($offset): void
475
    {
476
        unset($this->children[$offset]);
477
    }
478
479
    /**
480
     * Countable interface
481
     *
482
     * @return int
483
     */
484 4
    public function count(): int
485
    {
486 4
        return count($this->children);
487
    }
488
489
    /**
490
     * Iterator interface
491
     *
492
     * @return mixed
493
     */
494 1
    public function current()
495
    {
496 1
        return $this->children[$this->position];
497
    }
498
499
    /**
500
     * Iterator interface
501
     */
502 1
    public function next(): void
503
    {
504 1
        ++$this->position;
505 1
    }
506
507
    /**
508
     * Iterator interface
509
     *
510
     * @return int
511
     */
512
    public function key(): int
513
    {
514
        return $this->position;
515
    }
516
517
    /**
518
     * Iterator interface
519
     *
520
     * @return bool
521
     */
522 1
    public function valid(): bool
523
    {
524 1
        return isset($this->children[$this->position]);
525
    }
526
527
    /**
528
     * Iterator interface
529
     */
530 1
    public function rewind(): void
531
    {
532 1
        $this->position = 0;
533 1
    }
534
}
535