Completed
Push — master ( 7cb780...532294 )
by
unknown
04:14 queued 02:10
created

Tree::getBreadcrumb()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 0
cts 14
cp 0
rs 9.2728
c 0
b 0
f 0
cc 5
nc 2
nop 0
crap 30
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
                } 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(array $outputLines)
251
    {
252
        // no children, empty folder or blob!
253 15
        if (count($this->children) > 0) {
254 10
            return;
255
        }
256
257
        // root, no blob
258 7
        if ($this->isRoot()) {
259
            return;
260
        }
261
262 7
        if (1 === count($outputLines)) {
263 7
            $treeObject = NodeObject::createFromOutputLine($this->repository, $outputLines[0]);
264 7
            if ($treeObject->getSha() === $this->subject->getSha()) {
265 7
                $this->blob = $treeObject;
266
            }
267
        }
268 7
    }
269
270
    /**
271
     * Reorder children of the tree
272
     * Tree first (alphabetically) and then blobs (alphabetically)
273
     *
274
     * @param \GitElephant\Objects\NodeObject $a the first object
275
     * @param \GitElephant\Objects\NodeObject $b the second object
276
     *
277
     * @return int
278
     */
279 7
    private function sortChildren(NodeObject $a, NodeObject $b)
280
    {
281 7
        if ($a->getType() == $b->getType()) {
282 5
            $names = [$a->getName(), $b->getName()];
283 5
            sort($names);
284
285 5
            return ($a->getName() === $names[0]) ? -1 : 1;
286
        }
287
288 5
        return $a->getType() == NodeObject::TYPE_TREE || $b->getType() == NodeObject::TYPE_BLOB ? -1 : 1;
289
    }
290
291
    /**
292
     * Parse a single line into pieces
293
     *
294
     * @param string $line a single line output from the git binary
295
     *
296
     * @return mixed
297
     */
298 15
    private function parseLine($line)
299
    {
300 15
        if ($line == '') {
301
            return;
302
        }
303
304 15
        $slices = NodeObject::getLineSlices($line);
305 15
        if ($this->isBlob()) {
306
            $this->pathChildren[] = $this->blob->getName();
307
        } else {
308 15
            if ($this->isRoot()) {
309
                // if is root check for first children
310 9
                $pattern = '/(\w+)\/(.*)/';
311 9
                $replacement = '$1';
312
            } else {
313
                // filter by the children of the path
314 10
                $actualPath = $this->subject->getFullPath();
315 10
                if (!preg_match(sprintf('/^%s\/(\w*)/', preg_quote($actualPath, '/')), $slices['fullPath'])) {
316 7
                    return;
317
                }
318 5
                $pattern = sprintf('/^%s\/(\w*)/', preg_quote($actualPath, '/'));
319 5
                $replacement = '$1';
320
            }
321
322 10
            $name = preg_replace($pattern, $replacement, $slices['fullPath']);
323 10
            if (strpos($name, '/') !== false) {
324
                return;
325
            }
326
327 10
            if (!in_array($name, $this->pathChildren)) {
328 10
                $path = rtrim(rtrim($slices['fullPath'], $name), '/');
329 10
                $treeObject = new TreeObject(
330 10
                    $this->repository,
331 10
                    $slices['permissions'],
332 10
                    $slices['type'],
333 10
                    $slices['sha'],
334 10
                    $slices['size'],
335
                    $name,
336
                    $path
337
                );
338 10
                $this->children[] = $treeObject;
339 10
                $this->pathChildren[] = $name;
340
            }
341
        }
342 10
    }
343
344
    /**
345
     * get the last commit message for this tree
346
     *
347
     * @param string $ref
348
     *
349
     * @throws \RuntimeException
350
     * @return Commit\Message
351
     */
352
    public function getLastCommitMessage($ref = 'master')
353
    {
354
        return $this->getLastCommit($ref)->getMessage();
355
    }
356
357
    /**
358
     * get author of the last commit
359
     *
360
     * @param string $ref
361
     *
362
     * @throws \RuntimeException
363
     * @return Author
364
     */
365
    public function getLastCommitAuthor($ref = 'master')
366
    {
367
        return $this->getLastCommit($ref)->getAuthor();
368
    }
369
370
    /**
371
     * get the last commit for a given treeish, for the actual tree
372
     *
373
     * @param string $ref
374
     *
375
     * @throws \RuntimeException
376
     * @throws \Symfony\Component\Process\Exception\RuntimeException
377
     * @return Commit
378
     */
379
    public function getLastCommit($ref = 'master')
380
    {
381
        if ($this->isRoot()) {
382
            return $this->getRepository()->getCommit($ref);
383
        }
384
        $log = $this->repository->getObjectLog($this->getObject(), $ref);
385
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()
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()
405
    {
406 5
        return $this->blob;
407
    }
408
409
    /**
410
     * Get Subject
411
     *
412
     * @return \GitElephant\Objects\NodeObject
413
     */
414 1
    public function getSubject()
415
    {
416 1
        return $this->subject;
417
    }
418
419
    /**
420
     * Get Ref
421
     *
422
     * @return string
423
     */
424
    public function getRef()
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)
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)
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)
475
    {
476
        unset($this->children[$offset]);
477
    }
478
479
    /**
480
     * Countable interface
481
     *
482
     * @return int
483
     */
484 4
    public function count()
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()
503
    {
504 1
        ++$this->position;
505 1
    }
506
507
    /**
508
     * Iterator interface
509
     *
510
     * @return int
511
     */
512
    public function key()
513
    {
514
        return $this->position;
515
    }
516
517
    /**
518
     * Iterator interface
519
     *
520
     * @return bool
521
     */
522 1
    public function valid()
523
    {
524 1
        return isset($this->children[$this->position]);
525
    }
526
527
    /**
528
     * Iterator interface
529
     */
530 1
    public function rewind()
531
    {
532 1
        $this->position = 0;
533 1
    }
534
}
535