Repository::getStatus()   B
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 30
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 18
cts 18
cp 1
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 20
nc 2
nop 0
crap 3
1
<?php
2
/*
3
 * Copyright (C) 2017 by TEQneers GmbH & Co. KG
4
 *
5
 * Permission is hereby granted, free of charge, to any person obtaining a copy
6
 * of this software and associated documentation files (the "Software"), to deal
7
 * in the Software without restriction, including without limitation the rights
8
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
 * copies of the Software, and to permit persons to whom the Software is
10
 * furnished to do so, subject to the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be included in
13
 * all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
 * THE SOFTWARE.
22
 */
23
24
/**
25
 * Git Stream Wrapper for PHP
26
 *
27
 * @category   TQ
28
 * @package    TQ_VCS
29
 * @subpackage Git
30
 * @copyright  Copyright (C) 2018 by TEQneers GmbH & Co. KG
31
 */
32
33
namespace TQ\Git\Repository;
34
use TQ\Vcs\FileSystem;
35
use TQ\Vcs\Repository\AbstractRepository;
36
use TQ\Git\Cli\Binary;
37
use TQ\Vcs\Cli\CallResult;
38
39
/**
40
 * Provides access to a Git repository
41
 *
42
 * @uses       \TQ\Git\Cli\Binary
43
 * @author     Stefan Gehrig <gehrigteqneers.de>
44
 * @category   TQ
45
 * @package    TQ_VCS
46
 * @subpackage Git
47
 * @copyright  Copyright (C) 2018 by TEQneers GmbH & Co. KG
48
 */
49
class Repository extends AbstractRepository
50
{
51
    const RESET_STAGED  = 1;
52
    const RESET_WORKING = 2;
53
    const RESET_ALL     = 3;
54
55
    const BRANCHES_LOCAL    = 1;
56
    const BRANCHES_REMOTE   = 2;
57
    const BRANCHES_ALL      = 3;
58
59
    /**
60
     * The Git binary
61
     *
62
     * @var Binary
63
     */
64
    protected $git;
65
66
        /**
67
     * Opens a Git repository on the file system, optionally creates and initializes a new repository
68
     *
69
     * @param   string               $repositoryPath        The full path to the repository
70
     * @param   Binary|string|null   $git                   The Git binary
71
     * @param   boolean|integer      $createIfNotExists     False to fail on non-existing repositories, directory
72
     *                                                      creation mode, such as 0755  if the command
73
     *                                                      should create the directory and init the repository instead
74
     * @param   array|null           $initArguments         Arguments to be passed to git-init if initializing a
75
     *                                                      repository
76
     * @param   boolean              $findRepositoryRoot    False to use the repository path as the root directory.
77
     *
78
     * @return  Repository
79
     * @throws  \RuntimeException                       If the path cannot be created
80
     * @throws  \InvalidArgumentException               If the path is not valid or if it's not a valid Git repository
81
     */
82 103
    public static function open($repositoryPath, $git = null, $createIfNotExists = false, $initArguments = null, $findRepositoryRoot = true)
83
    {
84 103
        $git = Binary::ensure($git);
85 103
        $repositoryRoot = null;
86
87 103
        if (!is_string($repositoryPath)) {
88
            throw new \InvalidArgumentException(sprintf(
89
                '"%s" is not a valid path', $repositoryPath
90
            ));
91
        }
92
93 103
        if ($findRepositoryRoot) {
94 103
            $repositoryRoot = self::findRepositoryRoot($repositoryPath);
95
        }
96
97 103
        if ($repositoryRoot === null) {
98 4
            if (!$createIfNotExists) {
99 2
                throw new \InvalidArgumentException(sprintf(
100 2
                    '"%s" is not a valid path', $repositoryPath
101
                ));
102
            } else {
103 2
                if (!file_exists($repositoryPath) && !mkdir($repositoryPath, $createIfNotExists, true)) {
104
                    throw new \RuntimeException(sprintf(
105
                        '"%s" cannot be created', $repositoryPath
106
                    ));
107 2
                } else if (!is_dir($repositoryPath)) {
108
                    throw new \InvalidArgumentException(sprintf(
109
                        '"%s" is not a valid path', $repositoryPath
110
                    ));
111
                }
112 2
                self::initRepository($git, $repositoryPath, $initArguments);
113 2
                $repositoryRoot = $repositoryPath;
114
            }
115
        }
116
117 101
        if ($repositoryRoot === null) {
118
            throw new \InvalidArgumentException(sprintf(
119
                '"%s" is not a valid Git repository', $repositoryPath
120
            ));
121
        }
122
123 101
        return new static($repositoryRoot, $git);
124
    }
125
126
    /**
127
     * Initializes a path to be used as a Git repository
128
     *
129
     * @param   Binary   $git           The Git binary
130
     * @param   string   $path          The repository path
131
     * @param   array    $initArguments Arguments to pass to git-init when initializing the repository
132
     */
133 2
    protected static function initRepository(Binary $git, $path, $initArguments = null)
134
    {
135 2
        $initArguments = $initArguments ?: Array();
136
137
        /** @var $result CallResult */
138 2
        $result = $git->{'init'}($path, $initArguments);
139 2
        $result->assertSuccess(sprintf('Cannot initialize a Git repository in "%s"', $path));
140 2
    }
141
142
    /**
143
     * Tries to find the root directory for a given repository path
144
     *
145
     * @param   string      $path       The file system path
146
     * @return  string|null             NULL if the root cannot be found, the root path otherwise
147
     */
148 103
    public static function findRepositoryRoot($path)
149
    {
150
        return FileSystem::bubble($path, function($p) {
151 103
            $gitDir = $p.'/'.'.git';
152 103
            return file_exists($gitDir) && is_dir($gitDir);
153 103
        });
154
    }
155
156
    /**
157
     * Creates a new repository instance - use {@see open()} instead
158
     *
159
     * @param   string     $repositoryPath
160
     * @param   Binary     $git
161
     */
162 101
    protected function __construct($repositoryPath, Binary $git)
163
    {
164 101
        $this->git   = $git;
165 101
        parent::__construct($repositoryPath);
166 101
    }
167
168
    /**
169
     * Returns the Git binary
170
     *
171
     * @return  Binary
172
     */
173 78
    public function getGit()
174
    {
175 78
        return $this->git;
176
    }
177
178
    /**
179
     * Returns the current commit hash
180
     *
181
     * @return  string
182
     */
183 54
    public function getCurrentCommit()
184
    {
185
        /** @var $result CallResult */
186 54
        $result = $this->getGit()->{'rev-parse'}($this->getRepositoryPath(), array(
187 54
             '--verify',
188
            'HEAD'
189
        ));
190 54
        $result->assertSuccess(sprintf('Cannot rev-parse "%s"', $this->getRepositoryPath()));
191 54
        return $result->getStdOut();
192
    }
193
194
    /**
195
     * Commits the currently staged changes into the repository
196
     *
197
     * @param   string       $commitMsg         The commit message
198
     * @param   array|null   $file              Restrict commit to the given files or NULL to commit all staged changes
199
     * @param   array        $extraArgs         Allow the user to pass extra args eg array('-i')
200
     * @param   string|null  $author            The author
201
     */
202 55
    public function commit($commitMsg, array $file = null, $author = null, array $extraArgs = array())
203
    {
204 55
        $author = $author ?: $this->getAuthor();
205
        $args   = array(
206 55
            '--message'   => $commitMsg
207
        );
208 55
        if ($author !== null) {
209 5
            $args['--author']  = $author;
210
        }
211
212 55
        foreach($extraArgs as $value) {
213
           $args[] = $value;
214
        }
215
216 55
        if ($file !== null) {
217 53
            $args[] = '--';
218 53
            $args   = array_merge($args, $this->resolveLocalPath($file));
219
        }
220
221
        /** @var $result CallResult */
222 55
        $result = $this->getGit()->{'commit'}($this->getRepositoryPath(), $args);
223 55
        $result->assertSuccess(sprintf('Cannot commit to "%s"', $this->getRepositoryPath()));
224 55
    }
225
226
    /**
227
     * Resets the working directory and/or the staging area and discards all changes
228
     *
229
     * @param   integer     $what       Bit mask to indicate which parts should be reset
230
     */
231 2
    public function reset($what = self::RESET_ALL)
232
    {
233 2
        $what   = (int)$what;
234 2
        if (($what & self::RESET_STAGED) == self::RESET_STAGED) {
235
            /** @var $result CallResult */
236 2
            $result = $this->getGit()->{'reset'}($this->getRepositoryPath(), array('--hard'));
237 2
            $result->assertSuccess(sprintf('Cannot reset "%s"', $this->getRepositoryPath()));
238
        }
239
240 2
        if (($what & self::RESET_WORKING) == self::RESET_WORKING) {
241
            /** @var $result CallResult */
242 2
            $result = $this->getGit()->{'clean'}($this->getRepositoryPath(), array(
243 2
                '--force',
244
                '-x',
245
                '-d'
246
            ));
247 2
            $result->assertSuccess(sprintf('Cannot clean "%s"', $this->getRepositoryPath()));
248
        }
249 2
    }
250
251
    /**
252
     * Adds one or more files to the staging area
253
     *
254
     * @param   array   $file       The file(s) to be added or NULL to add all new and/or changed files to the staging area
255
     * @param   boolean $force
256
     */
257 40
    public function add(array $file = null, $force = false)
258
    {
259 40
        $args   = array();
260 40
        if ($force) {
261
            $args[]  = '--force';
262
        }
263 40
        if ($file !== null) {
264 37
            $args[] = '--';
265 37
            $args   = array_merge($args, array_map(array($this, 'translatePathspec'), $this->resolveLocalPath($file)));
266
        } else {
267 3
            $args[] = '--all';
268
        }
269
270
        /** @var $result CallResult */
271 40
        $result = $this->getGit()->{'add'}($this->getRepositoryPath(), $args);
272 40
        $result->assertSuccess(sprintf('Cannot add "%s" to "%s"',
273 40
            ($file !== null) ? implode(', ', $file) : '*', $this->getRepositoryPath()
274
        ));
275 40
    }
276
277
    /**
278
     * Removes one or more files from the repository but does not commit the changes
279
     *
280
     * @param   array   $file           The file(s) to be removed
281
     * @param   boolean $recursive      True to recursively remove subdirectories
282
     * @param   boolean $force          True to continue even though Git reports a possible conflict
283
     */
284 13
    public function remove(array $file, $recursive = false, $force = false)
285
    {
286 13
        $args   = array();
287 13
        if ($recursive) {
288 5
            $args[] = '-r';
289
        }
290 13
        if ($force) {
291
            $args[] = '--force';
292
        }
293 13
        $args[] = '--';
294 13
        $args   = array_merge($args, $this->resolveLocalPath($file));
295
296
        /** @var $result CallResult */
297 13
        $result = $this->getGit()->{'rm'}($this->getRepositoryPath(), $args);
298 13
        $result->assertSuccess(sprintf('Cannot remove "%s" from "%s"',
299 13
            implode(', ', $file), $this->getRepositoryPath()
300
        ));
301 13
    }
302
303
    /**
304
     * Renames a file but does not commit the changes
305
     *
306
     * @param   string  $fromPath   The source path
307
     * @param   string  $toPath     The destination path
308
     * @param   boolean $force      True to continue even though Git reports a possible conflict
309
     */
310 9
    public function move($fromPath, $toPath, $force = false)
311
    {
312 9
        $args   = array();
313 9
        if ($force) {
314
            $args[] = '--force';
315
        }
316 9
        $args[] = $this->resolveLocalPath($fromPath);
317 9
        $args[] = $this->resolveLocalPath($toPath);
318
319
        /** @var $result CallResult */
320 9
        $result = $this->getGit()->{'mv'}($this->getRepositoryPath(), $args);
321 9
        $result->assertSuccess(sprintf('Cannot move "%s" to "%s" in "%s"',
322 9
            $fromPath, $toPath, $this->getRepositoryPath()
323
        ));
324 9
    }
325
326
    /**
327
     * Writes data to a file and commit the changes immediately
328
     *
329
     * @param   string          $path           The file path
330
     * @param   string|array    $data           The data to write to the file
331
     * @param   string|null     $commitMsg      The commit message used when committing the changes
332
     * @param   integer|null    $fileMode       The mode for creating the file
333
     * @param   integer|null    $dirMode        The mode for creating the intermediate directories
334
     * @param   boolean         $recursive      Create intermediate directories recursively if required
335
     * @param   string|null     $author         The author
336
     * @return  string                          The current commit hash
337
     * @throws  \RuntimeException               If the file could not be written
338
     */
339 24
    public function writeFile($path, $data, $commitMsg = null, $fileMode = null,
340
        $dirMode = null, $recursive = true, $author = null
341
    ) {
342 24
        $file       = $this->resolveFullPath($path);
343
344 24
        $fileMode   = $fileMode ?: $this->getFileCreationMode();
345 24
        $dirMode    = $dirMode ?: $this->getDirectoryCreationMode();
346
347 24
        $directory  = dirname($file);
348 24
        if (!file_exists($directory) && !mkdir($directory, (int)$dirMode, $recursive)) {
349
            throw new \RuntimeException(sprintf('Cannot create "%s"', $directory));
350 23
        } else if (!file_exists($file)) {
351 23
            if (!touch($file)) {
352
                throw new \RuntimeException(sprintf('Cannot create "%s"', $file));
353
            }
354 23
            if (!chmod($file, (int)$fileMode)) {
355
                throw new \RuntimeException(sprintf('Cannot chmod "%s" to %d', $file, (int)$fileMode));
356
            }
357
        }
358
359 23
        if (file_put_contents($file, $data) === false) {
360
            throw new \RuntimeException(sprintf('Cannot write to "%s"', $file));
361
        }
362
363 23
        $this->add(array($file));
364
365 23
        if ($commitMsg === null) {
366 20
            $commitMsg  = sprintf('%s created or changed file "%s"', __CLASS__, $path);
367
        }
368
369 23
        $this->commit($commitMsg, array($file), $author);
370
371 23
        return $this->getCurrentCommit();
372
    }
373
374
    /**
375
     * Writes data to a file and commit the changes immediately
376
     *
377
     * @param   string          $path           The directory path
378
     * @param   string|null     $commitMsg      The commit message used when committing the changes
379
     * @param   integer|null    $dirMode        The mode for creating the intermediate directories
380
     * @param   boolean         $recursive      Create intermediate directories recursively if required
381
     * @param   string|null     $author         The author
382
     * @return  string                          The current commit hash
383
     * @throws  \RuntimeException               If the directory could not be created
384
     */
385 4
    public function createDirectory($path, $commitMsg = null, $dirMode = null, $recursive = true, $author = null)
386
    {
387 4
        if ($commitMsg === null) {
388 3
            $commitMsg  = sprintf('%s created directory "%s"', __CLASS__, $path);
389
        }
390 4
        return $this->writeFile($path.'/.gitkeep', '', $commitMsg, 0666, $dirMode, $recursive, $author);
391
    }
392
393
    /**
394
     * Removes a file and commit the changes immediately
395
     *
396
     * @param   string          $path           The file path
397
     * @param   string|null     $commitMsg      The commit message used when committing the changes
398
     * @param   boolean         $recursive      True to recursively remove subdirectories
399
     * @param   boolean         $force          True to continue even though Git reports a possible conflict
400
     * @param   string|null     $author         The author
401
     * @return  string                          The current commit hash
402
     */
403 13
    public function removeFile($path, $commitMsg = null, $recursive = false, $force = false, $author = null)
404
    {
405 13
        $this->remove(array($path), $recursive, $force);
406
407 13
        if ($commitMsg === null) {
408 10
            $commitMsg  = sprintf('%s deleted file "%s"', __CLASS__, $path);
409
        }
410
411 13
        $this->commit($commitMsg, array($path), $author);
412
413 13
        return $this->getCurrentCommit();
414
    }
415
416
    /**
417
     * Renames a file and commit the changes immediately
418
     *
419
     * @param   string          $fromPath       The source path
420
     * @param   string          $toPath         The destination path
421
     * @param   string|null     $commitMsg      The commit message used when committing the changes
422
     * @param   boolean         $force          True to continue even though Git reports a possible conflict
423
     * @param   string|null     $author         The author
424
     * @return  string                          The current commit hash
425
     */
426 9
    public function renameFile($fromPath, $toPath, $commitMsg = null, $force = false, $author = null)
427
    {
428 9
        $this->move($fromPath, $toPath, $force);
429
430 9
        if ($commitMsg === null) {
431 8
            $commitMsg  = sprintf('%s renamed/moved file "%s" to "%s"', __CLASS__, $fromPath, $toPath);
432
        }
433
434 9
        $this->commit($commitMsg, array($fromPath, $toPath), $author);
435
436 9
        return $this->getCurrentCommit();
437
    }
438
439
    /**
440
     * Prepares a list of named arguments for use as command-line arguments.
441
     * Preserves ordering, while prepending - and -- to argument names, then leaves value alone.
442
     *
443
     * @param   array           $namedArguments    Named argument list to format
444
     * @return  array
445
     **/
446 2
    protected function _prepareNamedArgumentsForCLI($namedArguments) {
447 2
        $filteredArguments = array();
448 2
        $doneParsing = false;
449
450 2
        foreach ($namedArguments as $name => $value) {
451 2
            if ($value === false) {
452
                continue;
453
            }
454
455 2
            if (is_integer($name)) {
456 2
                $name = $value;
457 2
                $noValue = true;
458 2
            } elseif (is_bool($value)) {
459 2
                $noValue = true;
460 2
            } elseif (is_null($value)) {
461 2
                continue;
462
            } else {
463 2
                $noValue = false;
464
            }
465
466 2
            if ($name == '--') {
467 1
                $doneParsing = true;
468
            }
469
470 2
            if (!$doneParsing) {
471 2
                $name = preg_replace('{^(\w|\d+)$}', '-$0', $name);
472 2
                $name = preg_replace('{^[^-]}', '--$0', $name);
473
            }
474
475 2
            if ($noValue) {
476 2
                $filteredArguments[] = $name;
477 2
                continue;
478
            }
479
480 2
            $filteredArguments[$name] = $value;
481
        }
482
483 2
        return $filteredArguments;
484
    }
485
486
    /**
487
     * _parseNamedArguments
488
     *
489
     * Takes a set of regular arguments and a set of extended/named arguments, combines them, and returns the results.
490
     *
491
     * The merging method is far from foolproof, but should take care of the vast majority of situations.  Where it fails is function calls
492
     * in which the an argument is regular-style, is an array, and only has keys which are present in the named arguments.
493
     *
494
     * The easy way to trigger it would be to pass an empty array in one of the arguments.
495
     *
496
     * There's a bunch of array_splices.  Those are in place so that if named arguments have orders that they should be called in,
497
     * they're not disturbed.  So... calling with
498
     *      getLog(5, ['reverse', 'diff' => 'git', 'path/to/repo/file.txt']
499
     * will keep things in the order for the git call:
500
     *      git-log --limit=5 --skip=10 --reverse --diff=git path/to/to/repo/file.txt
501
     * and will put defaults at the beginning of the call, as well.
502
     *
503
     * @param   array $regularStyleArguments     An ordered list of the names of regular-style arguments that should be accepted.
504
     * @param   array $namedStyleArguments       An associative array of named arguments to their default value,
505
     *                                                 or null where no default is desired.
506
     * @param   array $arguments                 The result of func_get_args() in the original function call we're helping.
507
     * @param   int   $skipNamedTo               Index to which array arguments should be assumed NOT to be named arguments.
508
     * @return  array                            A filtered associative array of the resulting arguments.
509
     */
510 2
    protected function _parseNamedArguments($regularStyleArguments, $namedStyleArguments, $arguments, $skipNamedTo = 0) {
511 2
        $namedArguments = array();
512
513 2
        foreach ($regularStyleArguments as $name) {
514 2
            if (!isset($namedStyleArguments[$name])) {
515 2
                $namedStyleArguments[$name] = null;
516
            }
517
        }
518
519
        // We'll just step through the arguments and depending on whether the keys and values look appropriate, decide if they
520
        // are named arguments or regular arguments.
521 2
        foreach ($arguments as $index => $argument) {
522
            // If it's a named argument, we'll keep the whole thing.
523
            // Also keeps extra numbered arguments inside the named argument structure since they probably have special significance.
524 2
            if (is_array($argument) && $index >= $skipNamedTo) {
525 1
                $diff = array_diff_key($argument, $namedStyleArguments);
526 1
                $diffKeys = array_keys($diff);
527
528 1
                $integerDiffKeys = array_filter($diffKeys, 'is_int');
529 1
                $diffOnlyHasIntegerKeys = (count($diffKeys) === count($integerDiffKeys));
530
531 1
                if (empty($diff) || $diffOnlyHasIntegerKeys) {
532 1
                    $namedArguments = array_merge($namedArguments, $argument);
533 1
                    continue;
534
                }
535
536
                throw new \InvalidArgumentException('Unexpected named argument key: ' . implode(', ', $diffKeys));
537
            }
538
539 2
            if (empty($regularStyleArguments[$index])) {
540
                throw new \InvalidArgumentException("The argument parser received too many arguments!");
541
            }
542
543 2
            $name = $regularStyleArguments[$index];
544 2
            $namedArguments[$name] = $argument;
545
        }
546
547 2
        $defaultArguments = array_filter($namedStyleArguments,
548
            function($value) { return !is_null($value); }
549
        );
550
551
        // Insert defaults (for arguments that have no value) at the beginning
552 2
        $defaultArguments = array_diff_key($defaultArguments, $namedArguments);
553 2
        $namedArguments = array_merge($defaultArguments, $namedArguments);
554
555 2
        return $namedArguments;
556
    }
557
558
    /**
559
     * Returns the current repository log
560
     *
561
     * @param   integer|null    $limit      The maximum number of log entries returned
562
     * @param   integer|null    $skip       Number of log entries that are skipped from the beginning
563
     * @return  array
564
     */
565 2
    public function getLog($limit = null, $skip = null)
566
    {
567
        $regularStyleArguments = array(
568 2
            'limit',
569
            'skip'
570
        );
571
572
        $namedStyleArguments = array(
573 2
            'abbrev' => null,
574
            'abbrev-commit' => null,
575
            'after' => null,
576
            'all' => null,
577
            'all-match' => null,
578
            'ancestry-path' => null,
579
            'author' => null,
580
            'basic-regexp' => null,
581
            'before' => null,
582
            'binary' => null,
583
            'bisect' => null,
584
            'boundary' => null,
585
            'branches' => null,
586
            'break-rewrites' => null,
587
            'cc' => null,
588
            'check' => null,
589
            'cherry' => null,
590
            'cherry-mark' => null,
591
            'cherry-pick' => null,
592
            'children' => null,
593
            'color' => null,
594
            'color-words' => null,
595
            'combined' => null,
596
            'committer' => null,
597
            'date' => null,
598
            'date-order' => null,
599
            'decorate' => null,
600
            'dense' => null,
601
            'diff-filter' => null,
602
            'dirstat' => null,
603
            'do-walk' => null,
604
            'dst-prefix' => null,
605
            'encoding' => null,
606
            'exit-code' => null,
607
            'ext-diff' => null,
608
            'extended-regexp' => null,
609
            'find-copies' => null,
610
            'find-copies-harder' => null,
611
            'find-renames' => null,
612
            'first-parent' => null,
613
            'fixed-strings' => null,
614
            'follow' => null,
615
            'format' => 'fuller',
616
            'full-diff' => null,
617
            'full-history' => null,
618
            'full-index' => null,
619
            'function-context' => null,
620
            'glob' => null,
621
            'graph' => null,
622
            'grep' => null,
623
            'grep-reflog' => null,
624
            'histogram' => null,
625
            'ignore-all-space' => null,
626
            'ignore-missing' => null,
627
            'ignore-space-at-eol' => null,
628
            'ignore-space-change' => null,
629
            'ignore-submodules' => null,
630
            'inter-hunk-context' => null,
631
            'irreversible-delete' => null,
632
            'left-only' => null,
633
            'left-right' => null,
634
            'log-size' => null,
635
            'max-count' => null,
636
            'max-parents' => null,
637
            'merge' => null,
638
            'merges' => null,
639
            'min-parents' => null,
640
            'minimal' => null,
641
            'name-only' => null,
642
            'name-status' => null,
643
            'no-abbrev' => null,
644
            'no-abbrev-commit' => null,
645
            'no-color' => null,
646
            'no-decorate' => null,
647
            'no-ext-diff' => null,
648
            'no-max-parents' => null,
649
            'no-merges' => null,
650
            'no-min-parents' => null,
651
            'no-notes' => null,
652
            'no-prefix' => null,
653
            'no-renames' => null,
654
            'no-textconv' => null,
655
            'no-walk' => null,
656
            'not' => null,
657
            'notes' => null,
658
            'numstat' => null,
659
            'objects' => null,
660
            'objects-edge' => null,
661
            'oneline' => null,
662
            'parents' => null,
663
            'patch' => null,
664
            'patch-with-raw' => null,
665
            'patch-with-stat' => null,
666
            'patience' => null,
667
            'perl-regexp' => null,
668
            'pickaxe-all' => null,
669
            'pickaxe-regex' => null,
670
            'pretty' => null,
671
            'raw' => null,
672
            'regexp-ignore-case' => null,
673
            'relative' => null,
674
            'relative-date' => null,
675
            'remotes' => null,
676
            'remove-empty' => null,
677
            'reverse' => null,
678
            'right-only' => null,
679
            'shortstat' => null,
680
            'show-notes' => null,
681
            'show-signature' => null,
682
            'simplify-by-decoration' => null,
683
            'simplify-merges' => null,
684
            'since' => null,
685
            'skip' => null,
686
            'source' => null,
687
            'sparse' => null,
688
            'src-prefix' => null,
689
            'stat' => null,
690
            'stat-count' => null,
691
            'stat-graph-width' => null,
692
            'stat-name-width' => null,
693
            'stat-width' => null,
694
            'stdin' => null,
695
            'submodule' => null,
696
            'summary' => true,
697
            'tags' => null,
698
            'text' => null,
699
            'textconv' => null,
700
            'topo-order' => null,
701
            'unified' => null,
702
            'unpacked' => null,
703
            'until' => null,
704
            'verify' => null,
705
            'walk-reflogs' => null,
706
            'word-diff' => null,
707
            'word-diff-regex' => null,
708
            'z' => true
709
        );
710
711 2
        $arguments = func_get_args();
712
713 2
        $arguments = $this->_parseNamedArguments($regularStyleArguments, $namedStyleArguments, $arguments);
714
715 2
        if (!empty($arguments['limit'])) {
716 2
            $limit = '' . (int) $arguments['limit'];
717
718 2
            unset($arguments['limit']);
719 2
            array_unshift($arguments, $limit);
720
        }
721
722 2
        $arguments = $this->_prepareNamedArgumentsForCLI($arguments);
723
724
        /** @var $result CallResult */
725 2
        $result = $this->getGit()->{'log'}($this->getRepositoryPath(), $arguments);
726 2
        $result->assertSuccess(sprintf('Cannot retrieve log from "%s"',
727 2
            $this->getRepositoryPath()
728
        ));
729
730 2
        $output     = $result->getStdOut();
731
        $log        = array_map(function($f) {
732 2
            return trim($f);
733 2
        }, explode("\x0", $output));
734
735 2
        return $log;
736
    }
737
738
    /**
739
     * Returns a string containing information about the given commit
740
     *
741
     * @param  string  $hash       The commit ref
742
     * @return  string
743
     */
744 39
    public function showCommit($hash)
745
    {
746
        /** @var $result CallResult */
747 39
        $result = $this->getGit()->{'show'}($this->getRepositoryPath(), array(
748 39
            '--format' => 'fuller',
749 39
            $hash
750
        ));
751 39
        $result->assertSuccess(sprintf('Cannot retrieve commit "%s" from "%s"',
752 39
            $hash, $this->getRepositoryPath()
753
        ));
754
755 39
        return $result->getStdOut();
756
    }
757
758
    /**
759
     * Returns the content of a file at a given version
760
     *
761
     * @param   string  $file       The path to the file
762
     * @param   string  $ref        The version ref
763
     * @return  string
764
     */
765 5
    public function showFile($file, $ref = 'HEAD')
766
    {
767
        /** @var $result CallResult */
768 5
        $result = $this->getGit()->{'show'}($this->getRepositoryPath(), array(
769 5
            sprintf('%s:%s', $ref, $file)
770
        ));
771 5
        $result->assertSuccess(sprintf('Cannot show "%s" at "%s" from "%s"',
772 5
            $file, $ref, $this->getRepositoryPath()
773
        ));
774
775
776 5
        return $result->getStdOut();
777
    }
778
779
    /**
780
     * Returns information about an object at a given version
781
     *
782
     * The information returned is an array with the following structure
783
     * array(
784
     *      'type'  => blob|tree|commit,
785
     *      'mode'  => 0040000 for a tree, 0100000 for a blob, 0 otherwise,
786
     *      'size'  => the size
787
     * )
788
     *
789
     * @param   string  $path       The path to the object
790
     * @param   string  $ref        The version ref
791
     * @return  array               The object info
792
     */
793 6
    public function getObjectInfo($path, $ref = 'HEAD')
794
    {
795
        $info   = array(
796 6
            'type'  => null,
797
            'mode'  => 0,
798
            'size'  => 0
799
        );
800
801
        /** @var $result CallResult */
802 6
        $result = $this->getGit()->{'cat-file'}($this->getRepositoryPath(), array(
803 6
            '--batch-check'
804 6
        ), sprintf('%s:%s', $ref, $path));
805 6
        $result->assertSuccess(sprintf('Cannot cat-file "%s" at "%s" from "%s"',
806 6
            $path, $ref, $this->getRepositoryPath()
807
        ));
808 6
        $output = trim($result->getStdOut());
809
810 6
        $parts  = array();
811 6
        if (preg_match('/^(?<sha1>[0-9a-f]{40}) (?<type>\w+) (?<size>\d+)$/', $output, $parts)) {
812 6
            $mode   = 0;
813 6
            switch ($parts['type']) {
814 6
                case 'tree':
815 3
                    $mode   |= 0040000;
816 3
                    break;
817 4
                case 'blob':
818 4
                    $mode   |= 0100000;
819 4
                    break;
820
            }
821 6
            $info['sha1']   = $parts['sha1'];
822 6
            $info['type']   = $parts['type'];
823 6
            $info['mode']   = (int)$mode;
824 6
            $info['size']   = (int)$parts['size'];
825
        }
826 6
        return $info;
827
    }
828
829
    /**
830
     * List the directory at a given version
831
     *
832
     * @param   string  $directory      The path ot the directory
833
     * @param   string  $ref            The version ref
834
     * @return  array
835
     */
836 15
    public function listDirectory($directory = '.', $ref = 'HEAD')
837
    {
838 15
        $directory  = FileSystem::normalizeDirectorySeparator($directory);
839 15
        $directory  = $this->resolveLocalPath(rtrim($directory, '/') . '/');
840 15
        $path       = $this->getRepositoryPath();
841
842
        /** @var $result CallResult */
843 15
        $result = $this->getGit()->{'ls-tree'}($path, array(
844 15
            '--name-only',
845 15
            '--full-name',
846 15
            '-z',
847 15
            $ref,
848 15
            $this->translatePathspec($directory)
0 ignored issues
show
Bug introduced by
It seems like $directory defined by $this->resolveLocalPath(...$directory, '/') . '/') on line 839 can also be of type array; however, TQ\Git\Repository\Repository::translatePathspec() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
849
        ));
850 15
        $result->assertSuccess(sprintf('Cannot list directory "%s" at "%s" from "%s"',
851 15
            $directory, $ref, $this->getRepositoryPath()
852
        ));
853
854 15
        $output     = $result->getStdOut();
855
        $listing    = array_map(function($f) use ($directory) {
856 15
            return str_replace($directory, '', trim($f));
857 15
        }, explode("\x0", $output));
858 15
        return $listing;
859
    }
860
861
    /**
862
     * Returns the current status of the working directory and the staging area
863
     *
864
     * The returned array structure is
865
     *      array(
866
     *          'file'      => '...',
867
     *          'x'         => '.',
868
     *          'y'         => '.',
869
     *          'renamed'   => null/'...'
870
     *      )
871
     *
872
     * @return  array
873
     */
874 33
    public function getStatus()
875
    {
876
        /** @var $result CallResult */
877 33
        $result = $this->getGit()->{'status'}($this->getRepositoryPath(), array(
878 33
            '--short'
879
        ));
880 33
        $result->assertSuccess(
881 33
            sprintf('Cannot retrieve status from "%s"', $this->getRepositoryPath())
882
        );
883
884 33
        $output = rtrim($result->getStdOut());
885 33
        if (empty($output)) {
886 22
            return array();
887
        }
888
889
        $status = array_map(function($f) {
890 16
            $line   = rtrim($f);
891 16
            $parts  = array();
892 16
            preg_match('/^(?<x>.)(?<y>.)\s(?<f>.+?)(?:\s->\s(?<f2>.+))?$/', $line, $parts);
893
894
            $status = array(
895 16
                'file'      => $parts['f'],
896 16
                'x'         => trim($parts['x']),
897 16
                'y'         => trim($parts['y']),
898 16
                'renamed'   => (array_key_exists('f2', $parts)) ? $parts['f2'] : null
899
            );
900 16
            return $status;
901 16
        }, explode("\n", $output));
902 16
        return $status;
903
    }
904
905
    /**
906
     * Returns the diff of a file
907
     *
908
     * @param   string[]  $files      The path to the file
909
     * @param   bool      $staged     Should the diff return for the staged file
910
     * @return  string[]
911
     */
912 1
    public function getDiff(array $files = null, $staged = false)
913
    {
914 1
        $diffs = array();
915
916 1
        if (is_null($files)) {
917 1
            $files    = array();
918 1
            $status   = $this->getStatus();
919 1
            $modified = ($staged ? 'x' : 'y');
920
921 1
            foreach ($status as $entry) {
922 1
               if ($entry[$modified] !== 'M') {
923
                        continue;
924
                }
925
926 1
                $files[] = $entry['file'];
927
            }
928
        }
929
930 1
        $files = array_map(array($this, 'resolveLocalPath'), $files);
931
932 1
        foreach ($files as $file) {
933 1
            $args = array();
934
935 1
            if ($staged) {
936 1
                $args[] = '--staged';
937
            }
938
939 1
            $args[] = $this->translatePathspec($file);
940
941
            /** @var CallResult $result */
942 1
            $result = $this->getGit()->{'diff'}($this->getRepositoryPath(), $args);
943 1
            $result->assertSuccess(sprintf('Cannot show diff for %s from "%s"',
944 1
                $file, $this->getRepositoryPath()
945
            ));
946
947 1
            $diffs[$file] = $result->getStdOut();
948
        }
949
950 1
        return $diffs;
951
    }
952
953
    /**
954
     * Returns true if there are uncommitted changes in the working directory and/or the staging area
955
     *
956
     * @return  boolean
957
     */
958 33
    public function isDirty()
959
    {
960 33
        $status = $this->getStatus();
961 33
        return !empty($status);
962
    }
963
964
    /**
965
     * Returns the name of the current branch
966
     *
967
     * @return  string
968
     */
969 1
    public function getCurrentBranch()
970
    {
971
        /** @var $result CallResult */
972 1
        $result = $this->getGit()->{'rev-parse'}($this->getRepositoryPath(), array(
973 1
            '--symbolic-full-name',
974
            '--abbrev-ref',
975
            'HEAD'
976
        ));
977 1
        $result->assertSuccess(
978 1
            sprintf('Cannot retrieve current branch from "%s"', $this->getRepositoryPath())
979
        );
980
981 1
        return $result->getStdOut();
982
    }
983
984
    /**
985
     * Returns a list of the branches in the repository
986
     *
987
     * @param   integer     $which      Which branches to retrieve (all, local or remote-tracking)
988
     * @return  array
989
     */
990 1
    public function getBranches($which = self::BRANCHES_LOCAL)
991
    {
992 1
        $which       = (int)$which;
993
        $arguments  = array(
994 1
            '--no-color'
995
        );
996
997 1
        $local  = (($which & self::BRANCHES_LOCAL) == self::BRANCHES_LOCAL);
998 1
        $remote = (($which & self::BRANCHES_REMOTE) == self::BRANCHES_REMOTE);
999
1000 1
        if ($local && $remote) {
1001
            $arguments[] = '-a';
1002 1
        } else if ($remote) {
1003
            $arguments[] = '-r';
1004
        }
1005
1006
        /** @var $result CallResult */
1007 1
        $result = $this->getGit()->{'branch'}($this->getRepositoryPath(), $arguments);
1008 1
        $result->assertSuccess(
1009 1
            sprintf('Cannot retrieve branch from "%s"', $this->getRepositoryPath())
1010
        );
1011
1012 1
        $output = rtrim($result->getStdOut());
1013 1
        if (empty($output)) {
1014
            return array();
1015
        }
1016
1017 1
        $branches = array_map(function($b) {
1018 1
            $line   = rtrim($b);
1019 1
            if (strpos($line, '* ') === 0) {
1020 1
                $line   = substr($line, 2);
1021
            }
1022 1
            $line   = ltrim($line);
1023 1
            return $line;
1024 1
        }, explode("\n", $output));
1025 1
        return $branches;
1026
    }
1027
1028
    /**
1029
     * Returns the remote info
1030
     *
1031
     * @return  array
1032
     */
1033
    public function getCurrentRemote()
1034
    {
1035
        /** @var $result CallResult */
1036
        $result = $this->getGit()->{'remote'}($this->getRepositoryPath(), array(
1037
             '-v'
1038
        ));
1039
        $result->assertSuccess(sprintf('Cannot remote "%s"', $this->getRepositoryPath()));
1040
1041
        $tmp = $result->getStdOut();
1042
1043
        preg_match_all('/([a-z]*)\h(.*)\h\((.*)\)/', $tmp, $matches);
1044
1045
        $retVar = array();
1046
        foreach($matches[0] as $key => $value)
1047
            $retVar[$matches[1][$key]][$matches[3][$key]] = $matches[2][$key];
1048
1049
        return $retVar;
1050
    }
1051
1052
    /**
1053
     * Translates the application's path representation to a valid git pathspec
1054
     *
1055
     * @link https://git-scm.com/docs/gitglossary#gitglossary-aiddefpathspecapathspec
1056
     *
1057
     * @param string $path
1058
     * @return string
1059
     */
1060 49
    protected function translatePathspec($path)
1061
    {
1062
        // An empty string in this application's context means the current working directory.
1063
        // Due to breaking changes in git 2.16.0 (see https://github.com/git/git/blob/master/Documentation/RelNotes/2.16.0.txt)
1064
        // an empty path is no longer a valid pathspec but a dot
1065 49
        if ($path === '') {
1066 8
            $path = '.';
1067
        }
1068
1069 49
        return $path;
1070
    }
1071
}
1072