Completed
Push — master ( ab60f2...c901a1 )
by Ricardo
07:00
created

GitRepo::create_new()   B

Complexity

Conditions 9
Paths 7

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 8.0555
c 0
b 0
f 0
cc 9
nc 7
nop 4
1
<?php
2
3
namespace Fabrica\Helps\Git;
4
5
use DateTime;
6
7
/**
8
 * Git Repository Interface Class
9
 *
10
 * This class enables the creating, reading, and manipulation
11
 * of a git repository
12
 *
13
 * @class GitRepo
14
 */
15
class GitRepo
16
{
17
18
    protected $bare = false;
19
    protected $envopts = array();
20
21
    /**
22
     * @var string
23
     */
24
    private $repositoryPath = null;
25
26
    /**
27
     * Create a new git repository
28
     *
29
     * Accepts a creation path, and, optionally, a source path
30
     *
31
     * @access public
32
     * @param  string  repository path
33
     * @param  string  directory to source
34
     * @param  string  reference path
35
     * @return GitRepo
36
     */
37
    public static function &create_new($repositoryPath, $source = null, $remote_source = false, $reference = null)
38
    {
39
        if (is_dir($repositoryPath) && file_exists($repositoryPath."/.git")) {
40
            throw new Exception('"'.$repositoryPath.'" is already a git repository');
41
        } else {
42
            $repo = new self($repositoryPath, true, false);
43
            if (is_string($source)) {
44
                if ($remote_source) {
45
                    if (isset($reference)) {
46
                        if (!is_dir($reference) || !is_dir($reference.'/.git')) {
47
                               throw new Exception('"'.$reference.'" is not a git repository. Cannot use as reference.');
48
                        } else if (strlen($reference)) {
49
                            $reference = realpath($reference);
50
                            $reference = "--reference $reference";
51
                        }
52
                    }
53
                    $repo->clone_remote($source, $reference);
54
                } else {
55
                    $repo->clone_from($source);
56
                }
57
            } else {
58
                $repo->run('init');
59
            }
60
            return $repo;
61
        }
62
    }
63
64
    /**
65
     * Constructor
66
     *
67
     * Accepts a repository path
68
     *
69
     * @access public
70
     * @param  string  repository path
71
     * @param  bool    create if not exists?
72
     * @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...
73
     */
74
    public function __construct($repositoryPath = null, $create_new = false, $_init = true)
75
    {
76
        if (is_string($repositoryPath)) {
77
            $this->set_repositoryPath($repositoryPath, $create_new, $_init);
78
        }
79
    }
80
81
    /**
82
     * Set the repository's path
83
     *
84
     * Accepts the repository path
85
     *
86
     * @access public
87
     * @param  string  repository path
88
     * @param  bool    create if not exists?
89
     * @param  bool    initialize new Git repo if not exists?
90
     * @return void
91
     */
92
    public function set_repositoryPath($repositoryPath, $create_new = false, $_init = true)
93
    {
94
        if (is_string($repositoryPath)) {
95
            if ($new_path = realpath($repositoryPath)) {
96
                $repositoryPath = $new_path;
97
                if (is_dir($repositoryPath)) {
98
                    // Is this a work tree?
99
                    if (file_exists($repositoryPath."/.git")) {
100
                        $this->repositoryPath = $repositoryPath;
101
                        $this->bare = false;
102
                          // Is this a bare repo?
103
                    } else if (is_file($repositoryPath."/config")) {
104
                        $parse_ini = parse_ini_file($repositoryPath."/config");
105
                        if ($parse_ini['bare']) {
106
                               $this->repositoryPath = $repositoryPath;
107
                               $this->bare = true;
108
                        }
109
                    } else {
110
                        if ($create_new) {
111
                            $this->repositoryPath = $repositoryPath;
112
                            if ($_init) {
113
                                $this->run('init');
114
                            }
115
                        } else {
116
                            throw new Exception('"'.$repositoryPath.'" is not a git repository');
117
                        }
118
                    }
119
                } else {
120
                    throw new Exception('"'.$repositoryPath.'" is not a directory');
121
                }
122
            } else {
123
                if ($create_new) {
124
                    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...
125
                        mkdir($repositoryPath);
126
                        $this->repositoryPath = $repositoryPath;
127
                        if ($_init) { $this->run('init');
128
                        }
129
                    } else {
130
                        throw new Exception('cannot create repository in non-existent directory');
131
                    }
132
                } else {
133
                    throw new Exception('"'.$repositoryPath.'" does not exist');
134
                }
135
            }
136
        }
137
    }
138
    
139
    /**
140
     * Get the path to the git repo directory (eg. the ".git" directory)
141
     * 
142
     * @access public
143
     * @return string
144
     */
145
    public function git_directory_path()
146
    {
147
        if ($this->bare) {
148
            return $this->repositoryPath;
149
        } else if (is_dir($this->repositoryPath."/.git")) {
150
            return $this->repositoryPath."/.git";
151
        } else if (is_file($this->repositoryPath."/.git")) {
152
            $git_file = file_get_contents($this->repositoryPath."/.git");
153
            if(mb_ereg("^gitdir: (.+)$", $git_file, $matches)) {
154
                if($matches[1]) {
155
                    $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...
156
                    return $this->repositoryPath."/".$rel_git_path;
157
                }
158
            }
159
        }
160
        throw new Exception('could not find git dir for '.$this->repositoryPath.'.');
161
    }
162
163
    /**
164
     * Tests if git is installed
165
     *
166
     * @access public
167
     * @return bool
168
     */
169
    public function test_git()
170
    {
171
        $descriptorspec = array(
172
        1 => array('pipe', 'w'),
173
        2 => array('pipe', 'w'),
174
        );
175
        $pipes = array();
176
        $resource = proc_open(Git::get_bin(), $descriptorspec, $pipes);
177
178
        $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...
179
        $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...
180
        foreach ($pipes as $pipe) {
181
            fclose($pipe);
182
        }
183
184
        $status = trim(proc_close($resource));
185
        return ($status != 127);
186
    }
187
188
    /**
189
     * Run a command in the git repository
190
     *
191
     * Accepts a shell command to run
192
     *
193
     * @access protected
194
     * @param  string  command to run
195
     * @return string
196
     */
197
    protected function run_command($command)
198
    {
199
        $descriptorspec = array(
200
        1 => array('pipe', 'w'),
201
        2 => array('pipe', 'w'),
202
        );
203
        $pipes = array();
204
        /* Depending on the value of variables_order, $_ENV may be empty.
205
        * In that case, we have to explicitly set the new variables with
206
        * putenv, and call proc_open with env=null to inherit the reset
207
        * of the system.
208
        *
209
        * This is kind of crappy because we cannot easily restore just those
210
        * variables afterwards.
211
        *
212
        * If $_ENV is not empty, then we can just copy it and be done with it.
213
        */
214
        if(count($_ENV) === 0) {
215
            $env = null;
216
            foreach($this->envopts as $k => $v) {
217
                putenv(sprintf("%s=%s", $k, $v));
218
            }
219
        } else {
220
            $env = array_merge($_ENV, $this->envopts);
221
        }
222
        $cwd = $this->repositoryPath;
223
        $resource = proc_open($command, $descriptorspec, $pipes, $cwd, $env);
224
225
        $stdout = stream_get_contents($pipes[1]);
226
        $stderr = stream_get_contents($pipes[2]);
227
        foreach ($pipes as $pipe) {
228
            fclose($pipe);
229
        }
230
231
        $status = trim(proc_close($resource));
232
        if ($status) { throw new Exception($stderr . "\n" . $stdout); //Not all errors are printed to stderr, so include std out as well.
233
        }
234
235
        return $stdout;
236
    }
237
238
    /**
239
     * Run a git command in the git repository
240
     *
241
     * Accepts a git command to run
242
     *
243
     * @access public
244
     * @param  string  command to run
245
     * @return string
246
     */
247
    public function run($command)
248
    {
249
        return $this->run_command(Git::get_bin()." ".$command);
250
    }
251
252
    /**
253
     * Runs a 'git status' call
254
     *
255
     * Accept a convert to HTML bool
256
     *
257
     * @access public
258
     * @param  bool  return string with <br />
259
     * @return string
260
     */
261
    public function status($html = false)
262
    {
263
        $msg = $this->run("status");
264
        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...
265
            $msg = str_replace("\n", "<br />", $msg);
266
        }
267
        return $msg;
268
    }
269
270
    /**
271
     * Runs a `git add` call
272
     *
273
     * Accepts a list of files to add
274
     *
275
     * @access public
276
     * @param  mixed   files to add
277
     * @return string
278
     */
279
    public function add($files = "*")
280
    {
281
        if (is_array($files)) {
282
            $files = '"'.implode('" "', $files).'"';
283
        }
284
        return $this->run("add $files -v");
285
    }
286
287
    /**
288
     * Runs a `git rm` call
289
     *
290
     * Accepts a list of files to remove
291
     *
292
     * @access public
293
     * @param  mixed    files to remove
294
     * @param  Boolean  use the --cached flag?
295
     * @return string
296
     */
297
    public function rm($files = "*", $cached = false)
298
    {
299
        if (is_array($files)) {
300
            $files = '"'.implode('" "', $files).'"';
301
        }
302
        return $this->run("rm ".($cached ? '--cached ' : '').$files);
303
    }
304
305
306
    /**
307
     * Runs a `git commit` call
308
     *
309
     * Accepts a commit message string
310
     *
311
     * @access public
312
     * @param  string  commit message
313
     * @param  boolean  should all files be committed automatically (-a flag)
314
     * @return string
315
     */
316
    public function commit($message = "", $commit_all = true)
317
    {
318
        $flags = $commit_all ? '-av' : '-v';
319
        return $this->run("commit ".$flags." -m ".escapeshellarg($message));
320
    }
321
322
    /**
323
     * Runs a `git clone` call to clone the current repository
324
     * into a different directory
325
     *
326
     * Accepts a target directory
327
     *
328
     * @access public
329
     * @param  string  target directory
330
     * @return string
331
     */
332
    public function clone_to($target)
333
    {
334
        return $this->run("clone --local ".$this->repositoryPath." $target");
335
    }
336
337
    /**
338
     * Runs a `git clone` call to clone a different repository
339
     * into the current repository
340
     *
341
     * Accepts a source directory
342
     *
343
     * @access public
344
     * @param  string  source directory
345
     * @return string
346
     */
347
    public function clone_from($source)
348
    {
349
        return $this->run("clone --local $source ".$this->repositoryPath);
350
    }
351
352
    /**
353
     * Runs a `git clone` call to clone a remote repository
354
     * into the current repository
355
     *
356
     * Accepts a source url
357
     *
358
     * @access public
359
     * @param  string  source url
360
     * @param  string  reference path
361
     * @return string
362
     */
363
    public function clone_remote($source, $reference)
364
    {
365
        return $this->run("clone $reference $source ".$this->repositoryPath);
366
    }
367
368
    /**
369
     * Runs a `git clean` call
370
     *
371
     * Accepts a remove directories flag
372
     *
373
     * @access public
374
     * @param  bool    delete directories?
375
     * @param  bool    force clean?
376
     * @return string
377
     */
378
    public function clean($dirs = false, $force = false)
379
    {
380
        return $this->run("clean".(($force) ? " -f" : "").(($dirs) ? " -d" : ""));
381
    }
382
383
    /**
384
     * Runs a `git branch` call
385
     *
386
     * Accepts a name for the branch
387
     *
388
     * @access public
389
     * @param  string  branch name
390
     * @return string
391
     */
392
    public function create_branch($branch)
393
    {
394
        return $this->run("branch " . escapeshellarg($branch));
395
    }
396
397
    /**
398
     * Runs a `git branch -[d|D]` 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 delete_branch($branch, $force = false)
407
    {
408
        return $this->run("branch ".(($force) ? '-D' : '-d')." $branch");
409
    }
410
411
    /**
412
     * Runs a `git branch` call
413
     *
414
     * @access public
415
     * @param  bool    keep asterisk mark on active branch
416
     * @return array
417
     */
418
    public function list_branches($keep_asterisk = false)
419
    {
420
        $branchArray = explode("\n", $this->run("branch"));
421
        foreach($branchArray as $i => &$branch) {
422
            $branch = trim($branch);
423
            if (! $keep_asterisk) {
424
                $branch = str_replace("* ", "", $branch);
425
            }
426
            if ($branch == "") {
427
                unset($branchArray[$i]);
428
            }
429
        }
430
        return $branchArray;
431
    }
432
433
    /**
434
     * Lists remote branches (using `git branch -r`).
435
     *
436
     * Also strips out the HEAD reference (e.g. "origin/HEAD -> origin/master").
437
     *
438
     * @access public
439
     * @return array
440
     */
441
    public function list_remote_branches()
442
    {
443
        $branchArray = explode("\n", $this->run("branch -r"));
444
        foreach($branchArray as $i => &$branch) {
445
            $branch = trim($branch);
446
            if ($branch == "" || strpos($branch, 'HEAD -> ') !== false) {
447
                unset($branchArray[$i]);
448
            }
449
        }
450
        return $branchArray;
451
    }
452
453
    /**
454
     * Returns name of active branch
455
     *
456
     * @access public
457
     * @param  bool    keep asterisk mark on branch name
458
     * @return string
459
     */
460
    public function active_branch($keep_asterisk = false)
461
    {
462
        $branchArray = $this->list_branches(true);
463
        $active_branch = preg_grep("/^\*/", $branchArray);
464
        reset($active_branch);
465
        if ($keep_asterisk) {
466
            return current($active_branch);
467
        } else {
468
            return str_replace("* ", "", current($active_branch));
469
        }
470
    }
471
472
    /**
473
     * Runs a `git checkout` call
474
     *
475
     * Accepts a name for the branch
476
     *
477
     * @access public
478
     * @param  string  branch name
479
     * @return string
480
     */
481
    public function checkout($branch)
482
    {
483
        return $this->run("checkout " . escapeshellarg($branch));
484
    }
485
486
487
    /**
488
     * Runs a `git merge` call
489
     *
490
     * Accepts a name for the branch to be merged
491
     *
492
     * @access public
493
     * @param  string $branch
494
     * @return string
495
     */
496
    public function merge($branch)
497
    {
498
        return $this->run("merge " . escapeshellarg($branch) . " --no-ff");
499
    }
500
501
502
    /**
503
     * Runs a git fetch on the current branch
504
     *
505
     * @access public
506
     * @return string
507
     */
508
    public function fetch()
509
    {
510
        return $this->run("fetch");
511
    }
512
513
    /**
514
     * Add a new tag on the current position
515
     *
516
     * Accepts the name for the tag and the message
517
     *
518
     * @param  string $tag
519
     * @param  string $message
520
     * @return string
521
     */
522
    public function add_tag($tag, $message = null)
523
    {
524
        if (is_null($message)) {
525
            $message = $tag;
526
        }
527
        return $this->run("tag -a $tag -m " . escapeshellarg($message));
528
    }
529
530
    /**
531
     * List all the available repository tags.
532
     *
533
     * Optionally, accept a shell wildcard pattern and return only tags matching it.
534
     *
535
     * @access public
536
     * @param  string $pattern Shell wildcard pattern to match tags against.
537
     * @return array                Available repository tags.
538
     */
539
    public function list_tags($pattern = null)
540
    {
541
        $tagArray = explode("\n", $this->run("tag -l $pattern"));
542
        foreach ($tagArray as $i => &$tag) {
543
            $tag = trim($tag);
544
            if (empty($tag)) {
545
                unset($tagArray[$i]);
546
            }
547
        }
548
549
        return $tagArray;
550
    }
551
552
    /**
553
     * Push specific branch (or all branches) to a remote
554
     *
555
     * Accepts the name of the remote and local branch.
556
     * If omitted, the command will be "git push", and therefore will take 
557
     * on the behavior of your "push.defualt" configuration setting.
558
     *
559
     * @param  string $remote
560
     * @param  string $branch
561
     * @return string
562
     */
563
    public function push($remote = "", $branch = "")
564
    {
565
                //--tags removed since this was preventing branches from being pushed (only tags were)
566
        return $this->run("push $remote $branch");
567
    }
568
569
    /**
570
     * Pull specific branch from remote
571
     *
572
     * Accepts the name of the remote and local branch.
573
     * If omitted, the command will be "git pull", and therefore will take on the
574
     * behavior as-configured in your clone / environment.
575
     *
576
     * @param  string $remote
577
     * @param  string $branch
578
     * @return string
579
     */
580
    public function pull($remote = "", $branch = "")
581
    {
582
        return $this->run("pull $remote $branch");
583
    }
584
585
    /**
586
     * List log entries.
587
     *
588
     * @param  strgin $format
589
     * @return string
590
     */
591
    public function log($format = null, $fulldiff=false, $filepath=null, $follow=false)
592
    {
593
        $diff = "";
594
        
595
        if ($fulldiff) {
596
            $diff = "--full-diff -p ";
597
        }
598
599
        if ($follow) {
600
            // Can't use full-diff with follow
601
            $diff = "--follow -- ";
602
        }
603
    
604
        if ($format === null) {
605
            return $this->run('log ' . $diff . $filepath);
606
        } else {
607
            return $this->run('log --pretty=format:"' . $format . '" ' . $diff .$filepath);
608
        }
609
    }
610
611
    /**
612
     * Sets the project description.
613
     *
614
     * @param string $new
615
     */
616
    public function set_description($new)
617
    {
618
        $path = $this->git_directory_path();
619
        file_put_contents($path."/description", $new);
620
    }
621
622
    /**
623
     * Gets the project description.
624
     *
625
     * @return string
626
     */
627
    public function get_description()
628
    {
629
        $path = $this->git_directory_path();
630
        return file_get_contents($path."/description");
631
    }
632
633
    /**
634
     * Sets custom environment options for calling Git
635
     *
636
     * @param string key
637
     * @param string value
638
     */
639
    public function setenv($key, $value)
640
    {
641
        $this->envopts[$key] = $value;
642
    }
643
644
    /**
645
     * @param string $revision
646
     */
647
    public function checkoutForce($revision)
648
    {
649
        $this->execute(
650
            'checkout --force --quiet ' . $revision
651
        );
652
    }
653
654
    /**
655
     * @return string
656
     */
657
    public function getCurrentBranch()
658
    {
659
        $output = $this->execute('symbolic-ref --short HEAD');
660
661
        return $output[0];
662
    }
663
664
    /**
665
     * @param  string $from
666
     * @param  string $to
667
     * @return string
668
     */
669
    public function getDiff($from, $to)
670
    {
671
        $output = $this->execute(
672
            'diff --no-ext-diff ' . $from . ' ' . $to
673
        );
674
675
        return implode("\n", $output);
676
    }
677
678
    /**
679
     * @return array
680
     */
681
    public function getRevisions()
682
    {
683
        $output = $this->execute(
684
            'log --no-merges --date-order --reverse --format=medium'
685
        );
686
687
        $numLines  = count($output);
688
        $revisions = array();
689
690
        for ($i = 0; $i < $numLines; $i++) {
691
            $tmp = explode(' ', $output[$i]);
692
693
            if ($tmp[0] == 'commit') {
694
                $sha1 = $tmp[1];
695
            } elseif ($tmp[0] == 'Author:') {
696
                $author = implode(' ', array_slice($tmp, 1));
697
            } elseif ($tmp[0] == 'Date:' && isset($author) && isset($sha1)) {
698
                $revisions[] = array(
699
                  'author'  => $author,
700
                'date'    => DateTime::createFromFormat(
701
                    'D M j H:i:s Y O',
702
                    implode(' ', array_slice($tmp, 3))
703
                ),
704
                  'sha1'    => $sha1,
705
                  'message' => isset($output[$i+2]) ? trim($output[$i+2]) : ''
706
                );
707
708
                unset($author);
709
                unset($sha1);
710
            }
711
        }
712
713
        return $revisions;
714
    }
715
716
    /**
717
     * @return bool
718
     */
719
    public function isWorkingCopyClean()
720
    {
721
        $output = $this->execute('status');
722
723
        return $output[count($output)-1] == 'nothing to commit, working directory clean' ||
724
               $output[count($output)-1] == 'nothing to commit, working tree clean';
725
    }
726
727
    /**
728
     * @param string $command
729
     *
730
     * @return string
731
     *
732
     * @throws RuntimeException
733
     */
734
    protected function execute($command)
735
    {
736
        $command = 'cd ' . escapeshellarg($this->repositoryPath) . '; git ' . $command . ' 2>&1';
737
 
738
        if (DIRECTORY_SEPARATOR == '/') {
739
            $command = 'LC_ALL=en_US.UTF-8 ' . $command;
740
        }
741
742
        exec($command, $output, $returnValue);
743
744
        if ($returnValue !== 0) {
745
            throw new RuntimeException(implode("\r\n", $output));
746
        }
747
748
        return $output;
749
    }
750
}