Completed
Push — master ( e13b70...f3b6bd )
by Stefan
06:13
created

Repository::showFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 2
crap 1
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
    public static function findRepositoryRoot($path)
149
    {
150 103
        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, $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 2
        $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
            $directory
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 15
        $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 16
        $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  $file       The path to the file
0 ignored issues
show
Bug introduced by
There is no parameter named $file. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
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[] = $file;
940
941 1
            $result = $this->getGit()->{'diff'}($this->getRepositoryPath(), $args);
942 1
            $result->assertSuccess(sprintf('Cannot show diff for %s from "%s"',
943 1
                $file, $this->getRepositoryPath()
944
            ));
945
946 1
            $diffs[$file] = $result->getStdOut();
947
        }
948
949 1
        return $diffs;
950
    }
951
952
    /**
953
     * Returns true if there are uncommitted changes in the working directory and/or the staging area
954
     *
955
     * @return  boolean
956
     */
957 33
    public function isDirty()
958
    {
959 33
        $status = $this->getStatus();
960 33
        return !empty($status);
961
    }
962
963
    /**
964
     * Returns the name of the current branch
965
     *
966
     * @return  string
967
     */
968 1
    public function getCurrentBranch()
969
    {
970
        /** @var $result CallResult */
971 1
        $result = $this->getGit()->{'rev-parse'}($this->getRepositoryPath(), array(
972 1
            '--symbolic-full-name',
973
            '--abbrev-ref',
974
            'HEAD'
975
        ));
976 1
        $result->assertSuccess(
977 1
            sprintf('Cannot retrieve current branch from "%s"', $this->getRepositoryPath())
978
        );
979
980 1
        return $result->getStdOut();
981
    }
982
983
    /**
984
     * Returns a list of the branches in the repository
985
     *
986
     * @param   integer     $which      Which branches to retrieve (all, local or remote-tracking)
987
     * @return  array
988
     */
989 1
    public function getBranches($which = self::BRANCHES_LOCAL)
990
    {
991 1
        $which       = (int)$which;
992
        $arguments  = array(
993 1
            '--no-color'
994
        );
995
996 1
        $local  = (($which & self::BRANCHES_LOCAL) == self::BRANCHES_LOCAL);
997 1
        $remote = (($which & self::BRANCHES_REMOTE) == self::BRANCHES_REMOTE);
998
999 1
        if ($local && $remote) {
1000
            $arguments[] = '-a';
1001 1
        } else if ($remote) {
1002
            $arguments[] = '-r';
1003
        }
1004
1005
        /** @var $result CallResult */
1006 1
        $result = $this->getGit()->{'branch'}($this->getRepositoryPath(), $arguments);
1007 1
        $result->assertSuccess(
1008 1
            sprintf('Cannot retrieve branch from "%s"', $this->getRepositoryPath())
1009
        );
1010
1011 1
        $output = rtrim($result->getStdOut());
1012 1
        if (empty($output)) {
1013
            return array();
1014
        }
1015
1016 1
        $branches = array_map(function($b) {
1017 1
            $line   = rtrim($b);
1018 1
            if (strpos($line, '* ') === 0) {
1019 1
                $line   = substr($line, 2);
1020
            }
1021 1
            $line   = ltrim($line);
1022 1
            return $line;
1023 1
        }, explode("\n", $output));
1024 1
        return $branches;
1025
    }
1026
1027
    /**
1028
     * Returns the remote info
1029
     *
1030
     * @return  array
1031
     */
1032
    public function getCurrentRemote()
1033
    {
1034
        /** @var $result CallResult */
1035
        $result = $this->getGit()->{'remote'}($this->getRepositoryPath(), array(
1036
             '-v'
1037
        ));
1038
        $result->assertSuccess(sprintf('Cannot remote "%s"', $this->getRepositoryPath()));
1039
1040
        $tmp = $result->getStdOut();
1041
1042
        preg_match_all('/([a-z]*)\h(.*)\h\((.*)\)/', $tmp, $matches);
1043
1044
        $retVar = array();
1045
        foreach($matches[0] as $key => $value)
1046
            $retVar[$matches[1][$key]][$matches[3][$key]] = $matches[2][$key];
1047
1048
        return $retVar;
1049
    }
1050
}
1051