Completed
Push — master ( 8c9cef...d36a0f )
by Ricardo
04:29
created

GitRepo::gitDirectoryPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Fabrica\Helps\Git;
4
5
use DateTime;
6
use Pedreiro\Exceptions\Exception;
7
use Pedreiro\Exceptions\RuntimeException;
8
9
/**
10
 * Git Repository Interface Class
11
 *
12
 * This class enables the creating, reading, and manipulation
13
 * of a git repository
14
 *
15
 * @class GitRepo
16
 */
17
class GitRepo
18
{
19
20
    protected $bare = false;
21
    protected $envopts = array();
22
23
    /**
24
     * @var string
25
     */
26
    private $repositoryPath = null;
27
28
    /**
29
     * Create a new git repository
30
     *
31
     * Accepts a creation path, and, optionally, a source path
32
     *
33
     * @access public
34
     * @param  string  repository path
35
     * @param  string  directory to source
36
     * @param  string  reference path
37
     * @return GitRepo
38
     */
39
    public static function &create_new($repositoryPath, $source = null, $remote_source = false, $reference = null)
40
    {
41
        if (is_dir($repositoryPath) && file_exists($repositoryPath."/.git")) {
42
            throw new Exception('"'.$repositoryPath.'" is already a git repository');
43
        } else {
44
            $repo = new self($repositoryPath, true, false);
45
            if (is_string($source)) {
46
                if ($remote_source) {
47
                    if (isset($reference)) {
48
                        if (!is_dir($reference) || !is_dir($reference.'/.git')) {
49
                               throw new Exception('"'.$reference.'" is not a git repository. Cannot use as reference.');
50
                        } else if (strlen($reference)) {
51
                            $reference = realpath($reference);
52
                            $reference = "--reference $reference";
53
                        }
54
                    }
55
                    $repo->clone_remote($source, $reference);
56
                } else {
57
                    $repo->clone_from($source);
58
                }
59
            } else {
60
                $repo->run('init');
61
            }
62
            return $repo;
63
        }
64
    }
65
66
    /**
67
     * Constructor
68
     *
69
     * Accepts a repository path
70
     *
71
     * @access public
72
     * @param  string  repository path
73
     * @param  bool    create if not exists?
74
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
75
     */
76
    public function __construct($repositoryPath = null, $create_new = false, $_init = true)
77
    {
78
        if (is_string($repositoryPath)) {
79
            $this->set_repositoryPath($repositoryPath, $create_new, $_init);
80
        }
81
    }
82
83
    /**
84
     * Set the repository's path
85
     *
86
     * Accepts the repository path
87
     *
88
     * @access public
89
     * @param  string  repository path
90
     * @param  bool    create if not exists?
91
     * @param  bool    initialize new Git repo if not exists?
92
     * @return void
93
     */
94
    public function set_repositoryPath($repositoryPath, $create_new = false, $_init = true)
95
    {
96
        if (is_string($repositoryPath)) {
97
            if ($new_path = realpath($repositoryPath)) {
98
                $repositoryPath = $new_path;
99
                if (is_dir($repositoryPath)) {
100
                    // Is this a work tree?
101
                    if (file_exists($repositoryPath."/.git")) {
102
                        $this->repositoryPath = $repositoryPath;
103
                        $this->bare = false;
104
                          // Is this a bare repo?
105
                    } else if (is_file($repositoryPath."/config")) {
106
                        $parse_ini = parse_ini_file($repositoryPath."/config");
107
                        if ($parse_ini['bare']) {
108
                               $this->repositoryPath = $repositoryPath;
109
                               $this->bare = true;
110
                        }
111
                    } else {
112
                        if ($create_new) {
113
                            $this->repositoryPath = $repositoryPath;
114
                            if ($_init) {
115
                                $this->run('init');
116
                            }
117
                        } else {
118
                            throw new Exception('"'.$repositoryPath.'" is not a git repository');
119
                        }
120
                    }
121
                } else {
122
                    throw new Exception('"'.$repositoryPath.'" is not a directory');
123
                }
124
            } else {
125
                if ($create_new) {
126
                    if ($parent = realpath(dirname($repositoryPath))) {
0 ignored issues
show
Unused Code introduced by
$parent is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
127
                        mkdir($repositoryPath);
128
                        $this->repositoryPath = $repositoryPath;
129
                        if ($_init) { $this->run('init');
130
                        }
131
                    } else {
132
                        throw new Exception('cannot create repository in non-existent directory');
133
                    }
134
                } else {
135
                    throw new Exception('"'.$repositoryPath.'" does not exist');
136
                }
137
            }
138
        }
139
    }
140
    
141
    /**
142
     * Get the path to the git repo directory (eg. the ".git" directory)
143
     * 
144
     * @access public
145
     * @return string
146
     */
147
    public function gitDirectoryPath()
148
    {
149
        return $this->git_directory_path();
150
    }
151
    
152
    /**
153
     * Get the path to the git repo directory (eg. the ".git" directory)
154
     * 
155
     * @access public
156
     * @return string
157
     */
158
    public function git_directory_path()
159
    {
160
161
        if ($this->bare) {
162
            return $this->repositoryPath;
163
        } else if (is_dir($this->repositoryPath."/.git")) {
164
            return $this->repositoryPath."/.git";
165
        } else if (is_file($this->repositoryPath."/.git")) {
166
            $git_file = file_get_contents($this->repositoryPath."/.git");
167
            if(mb_ereg("^gitdir: (.+)$", $git_file, $matches)) {
168
                if($matches[1]) {
169
                    $rel_git_path = $matches[1];
0 ignored issues
show
Bug introduced by
The variable $matches does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
170
                    return $this->repositoryPath."/".$rel_git_path;
171
                }
172
            }
173
        }
174
        throw new Exception('could not find git dir for '.$this->repositoryPath.'.');
175
    }
176
177
    /**
178
     * Tests if git is installed
179
     *
180
     * @access public
181
     * @return bool
182
     */
183
    public function test_git()
184
    {
185
        $descriptorspec = array(
186
        1 => array('pipe', 'w'),
187
        2 => array('pipe', 'w'),
188
        );
189
        $pipes = array();
190
        $resource = proc_open(Git::get_bin(), $descriptorspec, $pipes);
191
192
        $stdout = stream_get_contents($pipes[1]);
0 ignored issues
show
Unused Code introduced by
$stdout is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
193
        $stderr = stream_get_contents($pipes[2]);
0 ignored issues
show
Unused Code introduced by
$stderr is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
194
        foreach ($pipes as $pipe) {
195
            fclose($pipe);
196
        }
197
198
        $status = trim(proc_close($resource));
199
        return ($status != 127);
200
    }
201
202
    /**
203
     * Run a command in the git repository
204
     *
205
     * Accepts a shell command to run
206
     *
207
     * @access protected
208
     * @param  string  command to run
209
     * @return string
210
     */
211
    protected function run_command($command)
212
    {
213
        $descriptorspec = array(
214
        1 => array('pipe', 'w'),
215
        2 => array('pipe', 'w'),
216
        );
217
        $pipes = array();
218
        /* Depending on the value of variables_order, $_ENV may be empty.
219
        * In that case, we have to explicitly set the new variables with
220
        * putenv, and call proc_open with env=null to inherit the reset
221
        * of the system.
222
        *
223
        * This is kind of crappy because we cannot easily restore just those
224
        * variables afterwards.
225
        *
226
        * If $_ENV is not empty, then we can just copy it and be done with it.
227
        */
228
        if(count($_ENV) === 0) {
229
            $env = null;
230
            foreach($this->envopts as $k => $v) {
231
                putenv(sprintf("%s=%s", $k, $v));
232
            }
233
        } else {
234
            $env = array_merge($_ENV, $this->envopts);
235
        }
236
        $cwd = $this->repositoryPath;
237
        $resource = proc_open($command, $descriptorspec, $pipes, $cwd, $env);
238
239
        $stdout = stream_get_contents($pipes[1]);
240
        $stderr = stream_get_contents($pipes[2]);
241
        foreach ($pipes as $pipe) {
242
            fclose($pipe);
243
        }
244
245
        $status = trim(proc_close($resource));
246
        if ($status) { throw new Exception($stderr . "\n" . $stdout); //Not all errors are printed to stderr, so include std out as well.
247
        }
248
249
        return $stdout;
250
    }
251
252
    /**
253
     * Run a git command in the git repository
254
     *
255
     * Accepts a git command to run
256
     *
257
     * @access public
258
     * @param  string  command to run
259
     * @return string
260
     */
261
    public function run($command)
262
    {
263
        return $this->run_command(Git::get_bin()." ".$command);
264
    }
265
266
    /**
267
     * Runs a 'git status' call
268
     *
269
     * Accept a convert to HTML bool
270
     *
271
     * @access public
272
     * @param  bool  return string with <br />
273
     * @return string
274
     */
275
    public function status($html = false)
276
    {
277
        $msg = $this->run("status");
278
        if ($html == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
279
            $msg = str_replace("\n", "<br />", $msg);
280
        }
281
        return $msg;
282
    }
283
284
    /**
285
     * Runs a `git add` call
286
     *
287
     * Accepts a list of files to add
288
     *
289
     * @access public
290
     * @param  mixed   files to add
291
     * @return string
292
     */
293
    public function add($files = "*")
294
    {
295
        if (is_array($files)) {
296
            $files = '"'.implode('" "', $files).'"';
297
        }
298
        return $this->run("add $files -v");
299
    }
300
301
    /**
302
     * Runs a `git rm` call
303
     *
304
     * Accepts a list of files to remove
305
     *
306
     * @access public
307
     * @param  mixed    files to remove
308
     * @param  Boolean  use the --cached flag?
309
     * @return string
310
     */
311
    public function rm($files = "*", $cached = false)
312
    {
313
        if (is_array($files)) {
314
            $files = '"'.implode('" "', $files).'"';
315
        }
316
        return $this->run("rm ".($cached ? '--cached ' : '').$files);
317
    }
318
319
320
    /**
321
     * Runs a `git commit` call
322
     *
323
     * Accepts a commit message string
324
     *
325
     * @access public
326
     * @param  string  commit message
327
     * @param  boolean  should all files be committed automatically (-a flag)
328
     * @return string
329
     */
330
    public function commit($message = "", $commit_all = true)
331
    {
332
        $flags = $commit_all ? '-av' : '-v';
333
        return $this->run("commit ".$flags." -m ".escapeshellarg($message));
334
    }
335
336
    /**
337
     * Runs a `git clone` call to clone the current repository
338
     * into a different directory
339
     *
340
     * Accepts a target directory
341
     *
342
     * @access public
343
     * @param  string  target directory
344
     * @return string
345
     */
346
    public function clone_to($target)
347
    {
348
        return $this->run("clone --local ".$this->repositoryPath." $target");
349
    }
350
351
    /**
352
     * Runs a `git clone` call to clone a different repository
353
     * into the current repository
354
     *
355
     * Accepts a source directory
356
     *
357
     * @access public
358
     * @param  string  source directory
359
     * @return string
360
     */
361
    public function clone_from($source)
362
    {
363
        return $this->run("clone --local $source ".$this->repositoryPath);
364
    }
365
366
    /**
367
     * Runs a `git clone` call to clone a remote repository
368
     * into the current repository
369
     *
370
     * Accepts a source url
371
     *
372
     * @access public
373
     * @param  string  source url
374
     * @param  string  reference path
375
     * @return string
376
     */
377
    public function clone_remote($source, $reference)
378
    {
379
        return $this->run("clone $reference $source ".$this->repositoryPath);
380
    }
381
382
    /**
383
     * Runs a `git clean` call
384
     *
385
     * Accepts a remove directories flag
386
     *
387
     * @access public
388
     * @param  bool    delete directories?
389
     * @param  bool    force clean?
390
     * @return string
391
     */
392
    public function clean($dirs = false, $force = false)
393
    {
394
        return $this->run("clean".(($force) ? " -f" : "").(($dirs) ? " -d" : ""));
395
    }
396
397
    /**
398
     * Runs a `git branch` call
399
     *
400
     * Accepts a name for the branch
401
     *
402
     * @access public
403
     * @param  string  branch name
404
     * @return string
405
     */
406
    public function create_branch($branch)
407
    {
408
        return $this->run("branch " . escapeshellarg($branch));
409
    }
410
411
    /**
412
     * Runs a `git branch -[d|D]` call
413
     *
414
     * Accepts a name for the branch
415
     *
416
     * @access public
417
     * @param  string  branch name
418
     * @return string
419
     */
420
    public function delete_branch($branch, $force = false)
421
    {
422
        return $this->run("branch ".(($force) ? '-D' : '-d')." $branch");
423
    }
424
425
    
426
    /**
427
     * Runs a `git branch` call
428
     *
429
     * @access public
430
     * @param  bool    keep asterisk mark on active branch
431
     * @return array
432
     */
433
    public function listBranches($keep_asterisk = false)
434
    {
435
        return $this->list_branches($keep_asterisk);
436
    }
437
    /**
438
     * Runs a `git branch` call
439
     *
440
     * @access public
441
     * @param  bool    keep asterisk mark on active branch
442
     * @return array
443
     */
444
    public function list_branches($keep_asterisk = false)
445
    {
446
        $branchArray = explode("\n", $this->run("branch"));
447
        foreach($branchArray as $i => &$branch) {
448
            $branch = trim($branch);
449
            if (! $keep_asterisk) {
450
                $branch = str_replace("* ", "", $branch);
451
            }
452
            if ($branch == "") {
453
                unset($branchArray[$i]);
454
            }
455
        }
456
        return $branchArray;
457
    }
458
459
    /**
460
     * Lists remote branches (using `git branch -r`).
461
     *
462
     * Also strips out the HEAD reference (e.g. "origin/HEAD -> origin/master").
463
     *
464
     * @access public
465
     * @return array
466
     */
467
    public function list_remote_branches()
468
    {
469
        $branchArray = explode("\n", $this->run("branch -r"));
470
        foreach($branchArray as $i => &$branch) {
471
            $branch = trim($branch);
472
            if ($branch == "" || strpos($branch, 'HEAD -> ') !== false) {
473
                unset($branchArray[$i]);
474
            }
475
        }
476
        return $branchArray;
477
    }
478
479
    /**
480
     * Returns name of active branch
481
     *
482
     * @access public
483
     * @param  bool    keep asterisk mark on branch name
484
     * @return string
485
     */
486
    public function active_branch($keep_asterisk = false)
487
    {
488
        $branchArray = $this->list_branches(true);
489
        $active_branch = preg_grep("/^\*/", $branchArray);
490
        reset($active_branch);
491
        if ($keep_asterisk) {
492
            return current($active_branch);
493
        } else {
494
            return str_replace("* ", "", current($active_branch));
495
        }
496
    }
497
498
    /**
499
     * Runs a `git checkout` call
500
     *
501
     * Accepts a name for the branch
502
     *
503
     * @access public
504
     * @param  string  branch name
505
     * @return string
506
     */
507
    public function checkout($branch)
508
    {
509
        return $this->run("checkout " . escapeshellarg($branch));
510
    }
511
512
513
    /**
514
     * Runs a `git merge` call
515
     *
516
     * Accepts a name for the branch to be merged
517
     *
518
     * @access public
519
     * @param  string $branch
520
     * @return string
521
     */
522
    public function merge($branch)
523
    {
524
        return $this->run("merge " . escapeshellarg($branch) . " --no-ff");
525
    }
526
527
528
    /**
529
     * Runs a git fetch on the current branch
530
     *
531
     * @access public
532
     * @return string
533
     */
534
    public function fetch()
535
    {
536
        return $this->run("fetch");
537
    }
538
539
    /**
540
     * Add a new tag on the current position
541
     *
542
     * Accepts the name for the tag and the message
543
     *
544
     * @param  string $tag
545
     * @param  string $message
546
     * @return string
547
     */
548
    public function add_tag($tag, $message = null)
549
    {
550
        if (is_null($message)) {
551
            $message = $tag;
552
        }
553
        return $this->run("tag -a $tag -m " . escapeshellarg($message));
554
    }
555
556
    /**
557
     * List all the available repository tags.
558
     *
559
     * Optionally, accept a shell wildcard pattern and return only tags matching it.
560
     *
561
     * @access public
562
     * @param  string $pattern Shell wildcard pattern to match tags against.
563
     * @return array                Available repository tags.
564
     */
565
    public function list_tags($pattern = null)
566
    {
567
        $tagArray = explode("\n", $this->run("tag -l $pattern"));
568
        foreach ($tagArray as $i => &$tag) {
569
            $tag = trim($tag);
570
            if (empty($tag)) {
571
                unset($tagArray[$i]);
572
            }
573
        }
574
575
        return $tagArray;
576
    }
577
578
    /**
579
     * Push specific branch (or all branches) to a remote
580
     *
581
     * Accepts the name of the remote and local branch.
582
     * If omitted, the command will be "git push", and therefore will take 
583
     * on the behavior of your "push.defualt" configuration setting.
584
     *
585
     * @param  string $remote
586
     * @param  string $branch
587
     * @return string
588
     */
589
    public function push($remote = "", $branch = "")
590
    {
591
                //--tags removed since this was preventing branches from being pushed (only tags were)
592
        return $this->run("push $remote $branch");
593
    }
594
595
    /**
596
     * Pull specific branch from remote
597
     *
598
     * Accepts the name of the remote and local branch.
599
     * If omitted, the command will be "git pull", and therefore will take on the
600
     * behavior as-configured in your clone / environment.
601
     *
602
     * @param  string $remote
603
     * @param  string $branch
604
     * @return string
605
     */
606
    public function pull($remote = "", $branch = "")
607
    {
608
        return $this->run("pull $remote $branch");
609
    }
610
611
    /**
612
     * List log entries.
613
     *
614
     * @param  strgin $format
615
     * @return string
616
     */
617
    public function log($format = null, $fulldiff=false, $filepath=null, $follow=false)
618
    {
619
        $diff = "";
620
        
621
        if ($fulldiff) {
622
            $diff = "--full-diff -p ";
623
        }
624
625
        if ($follow) {
626
            // Can't use full-diff with follow
627
            $diff = "--follow -- ";
628
        }
629
    
630
        if ($format === null) {
631
            return $this->run('log ' . $diff . $filepath);
632
        } else {
633
            return $this->run('log --pretty=format:"' . $format . '" ' . $diff .$filepath);
634
        }
635
    }
636
637
    /**
638
     * Sets the project description.
639
     *
640
     * @param string $new
641
     */
642
    public function set_description($new)
643
    {
644
        $path = $this->git_directory_path();
645
        file_put_contents($path."/description", $new);
646
    }
647
648
    /**
649
     * Gets the project description.
650
     *
651
     * @return string
652
     */
653
    public function get_description()
654
    {
655
        $path = $this->git_directory_path();
656
        return file_get_contents($path."/description");
657
    }
658
659
    /**
660
     * Sets custom environment options for calling Git
661
     *
662
     * @param string key
663
     * @param string value
664
     */
665
    public function setenv($key, $value)
666
    {
667
        $this->envopts[$key] = $value;
668
    }
669
670
    /**
671
     * @param string $revision
672
     */
673
    public function checkoutForce($revision)
674
    {
675
        $this->execute(
676
            'checkout --force --quiet ' . $revision
677
        );
678
    }
679
680
    /**
681
     * @return string
682
     */
683
    public function getCurrentBranch()
684
    {
685
        $output = $this->execute('symbolic-ref --short HEAD');
686
687
        return $output[0];
688
    }
689
690
    /**
691
     * @param  string $from
692
     * @param  string $to
693
     * @return string
694
     */
695
    public function getDiff($from, $to)
696
    {
697
        $output = $this->execute(
698
            'diff --no-ext-diff ' . $from . ' ' . $to
699
        );
700
701
        return implode("\n", $output);
702
    }
703
704
    /**
705
     * @return array
706
     */
707
    public function getRevisions()
708
    {
709
        $output = $this->execute(
710
            'log --no-merges --date-order --reverse --format=medium'
711
        );
712
713
        $numLines  = count($output);
714
        $revisions = array();
715
716
        for ($i = 0; $i < $numLines; $i++) {
717
            $tmp = explode(' ', $output[$i]);
718
719
            if ($tmp[0] == 'commit') {
720
                $sha1 = $tmp[1];
721
            } elseif ($tmp[0] == 'Author:') {
722
                $author = implode(' ', array_slice($tmp, 1));
723
            } elseif ($tmp[0] == 'Date:' && isset($author) && isset($sha1)) {
724
                $revisions[] = array(
725
                  'author'  => $author,
726
                'date'    => DateTime::createFromFormat(
727
                    'D M j H:i:s Y O',
728
                    implode(' ', array_slice($tmp, 3))
729
                ),
730
                  'sha1'    => $sha1,
731
                  'message' => isset($output[$i+2]) ? trim($output[$i+2]) : ''
732
                );
733
734
                unset($author);
735
                unset($sha1);
736
            }
737
        }
738
739
        return $revisions;
740
    }
741
742
    /**
743
     * @return bool
744
     */
745
    public function isWorkingCopyClean()
746
    {
747
        $output = $this->execute('status');
748
749
        return $output[count($output)-1] == 'nothing to commit, working directory clean' ||
750
               $output[count($output)-1] == 'nothing to commit, working tree clean';
751
    }
752
753
    /**
754
     * @param string $command
755
     *
756
     * @return string
757
     *
758
     * @throws RuntimeException
759
     */
760
    protected function execute($command)
761
    {
762
        $command = 'cd ' . escapeshellarg($this->repositoryPath) . '; git ' . $command . ' 2>&1';
763
 
764
        if (DIRECTORY_SEPARATOR == '/') {
765
            $command = 'LC_ALL=en_US.UTF-8 ' . $command;
766
        }
767
768
        exec($command, $output, $returnValue);
769
770
        if ($returnValue !== 0) {
771
            throw new RuntimeException(implode("\r\n", $output));
772
        }
773
774
        return $output;
775
    }
776
}