Completed
Push — master ( 9b1383...69eea5 )
by Greg
01:24
created

WorkingCopy::switchBranch()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace Hubph\Git;
4
5
use Psr\Log\LoggerAwareInterface;
6
use Psr\Log\LoggerAwareTrait;
7
use Symfony\Component\Filesystem\Filesystem;
8
use Hubph\Util\ExecWithRedactionTrait;
9
10
class WorkingCopy implements LoggerAwareInterface
11
{
12
    use ExecWithRedactionTrait;
13
    use LoggerAwareTrait;
14
15
    protected $remote;
16
    protected $fork;
17
    protected $dir;
18
    protected $api;
19
20
    const FORCE_MERGE_COMMIT = 0x01;
21
22
    /**
23
     * WorkingCopy constructor
24
     *
25
     * @param $url Remote origin for the GitHub repository
26
     * @param $dir Checkout location for the project
27
     */
28
    protected function __construct($url, $dir, $branch = false, $api = null)
0 ignored issues
show
Unused Code introduced by
The parameter $branch is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
29
    {
30
        $this->remote = new Remote($url);
31
        $this->remote->addAuthentication($api);
32
        $this->dir = $dir;
33
        $this->api = $api;
34
35
        $this->confirmCachedRepoHasCorrectRemote();
36
    }
37
38
    public static function fromDir($dir, $remoteName = 'origin', $api = null)
39
    {
40
        $remote = Remote::fromDir($dir, $remoteName);
41
42
        return new self($remote->url(), $dir, false, $api);
43
    }
44
45
    /**
46
     * addFork will set a secondary remote on this repository.
47
     * The purpose of having a fork remote is if the primary repository
48
     * is read-only. If a fork is set, then any branches pushed
49
     * will go to the fork; any pull request created will still be
50
     * set on the primary repository, but will refer to the branch on
51
     * the fork.
52
     */
53
    public function addFork($fork_url)
54
    {
55
        if (empty($fork_url)) {
56
            $this->fork = null;
57
            return $this;
58
        }
59
        $this->fork = new Remote($fork_url);
60
        $this->fork->addAuthentication($this->api);
61
62
        return $this;
63
    }
64
65
    /**
66
     * createFork creates a new secondary repository copied from
67
     * the current repository, and sets it up as a fork per 'addFork'.
68
     */
69
    public function createFork($forked_project_name, $forked_org = '', $branch = '')
70
    {
71
        $result = $this->api->repoCreate(empty($forked_org) ? $this->org() : $forked_org, $forked_project_name);
72
73
        // 'git_url' => 'git://github.com/org/project.git',
74
        // 'ssh_url' => '[email protected]:org/project.git',
75
76
        $fork_url = $result['ssh_url'];
77
        $result = $this->addFork($fork_url);
78
79
        $this->push('fork', $branch);
80
81
        return $result;
82
    }
83
84
    public function deleteFork()
85
    {
86
        if (!$this->fork) {
87
            return;
88
        }
89
90
        $this->api->repoDelete($this->fork->org(), $this->fork->project());
91
    }
92
93
    /**
94
     * forkUrl returns the URL of the forked repository that should
95
     * be used for creating any pull requests.
96
     */
97
    public function forkUrl()
98
    {
99
        if (!$this->fork) {
100
            return null;
101
        }
102
        return $this->fork->url();
103
    }
104
105
    public function forkProjectWithOrg()
106
    {
107
        if (!$this->fork) {
108
            return null;
109
        }
110
        return $this->fork->projectWithOrg();
111
    }
112
113
    public function remoteFork()
114
    {
115
        if (!$this->fork) {
116
            return null;
117
        }
118
        return $this->fork;
119
    }
120
121
    /**
122
     * Clone the specified repository to the given URL at the indicated
123
     * directory. If the desired repository already exists there, then
124
     * we will re-use it rather than re-clone the repository.
125
     *
126
     * @param string $url
127
     * @param string $dir
128
     * @param HubphAPI|null $api
129
     * @return WorkingCopy
130
     */
131
    public static function clone($url, $dir, $api = null)
132
    {
133
        return static::cloneBranch($url, $dir, false, $api);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
134
    }
135
136
    /**
137
     * Clone the specified repository to the given URL at the indicated
138
     * directory. Only clone a single commit. Since we're only interested
139
     * in one commit, we'll just remove the cache if it is present.
140
     *
141
     * @param string $url
142
     * @param string $dir
143
     * @param string $branch
144
     * @param HubphAPI|null $api
145
     * @return WorkingCopy
146
     */
147
    public static function shallowClone($url, $dir, $branch, $depth = 1, $api = null)
148
    {
149
        $workingCopy = new self($url, $dir, $branch, $api);
0 ignored issues
show
Documentation introduced by
$branch is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
150
        $workingCopy->freshClone($branch, $depth);
0 ignored issues
show
Documentation introduced by
$branch is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
$depth is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
151
        return $workingCopy;
152
    }
153
154
    public static function unclonedReference($url, $dir, $branch, $api = null)
155
    {
156
        return new self($url, $dir, $branch, $api);
157
    }
158
159
    /**
160
     * Clone the specified branch of the specified repository to the given URL.
161
     *
162
     * @param string $url
163
     * @param string $dir
164
     * @param string $branch
165
     * @param HubphAPI|null $api
166
     * @return WorkingCopy
167
     */
168
    public static function cloneBranch($url, $dir, $branch, $api, $depth = false)
169
    {
170
        $workingCopy = new self($url, $dir, $branch, $api);
0 ignored issues
show
Documentation introduced by
$branch is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
171
        $workingCopy->cloneIfNecessary($branch, $depth);
0 ignored issues
show
Documentation introduced by
$branch is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
172
        return $workingCopy;
173
    }
174
175
    /**
176
     * take tranforms this local working copy such that it RETAINS all of its
177
     * local files (no change to any unstaged modifications or files) and
178
     * TAKES OVER the repository from the provided working copy.
179
     *
180
     * The local repository that was formerly in place here is disposed.
181
     * Any branches or commits not already pushed to the remote repository
182
     * are lost. Only the working files remain. The remotes for this working
183
     * copy become the remotes from the provided repository.
184
     *
185
     * The other working copy is disposed: its files are all removed
186
     * from the filesystem.
187
     */
188
    public function take(WorkingCopy $rhs)
189
    {
190
        $fs = new Filesystem();
191
192
        $ourLocalGitRepo = $this->dir() . '/.git';
193
        $rhsLocalGitRepo = $rhs->dir() . '/.git';
194
195
        $fs->remove($ourLocalGitRepo);
196
        $fs->rename($rhsLocalGitRepo, $ourLocalGitRepo);
197
198
        $this->remote = $rhs->remote();
199
        $this->addFork($rhs->forkUrl());
200
    }
201
202
    /**
203
     * remove will delete all of the local working files managed by this
204
     * object, including the '.git' directory. This method should be called
205
     * if the local working copy is corrupted or otherwise becomes unusable.
206
     */
207
    public function remove()
208
    {
209
        $fs = new Filesystem();
210
        $fs->remove($this->dir());
211
    }
212
213
    public function remote($remote_name = '')
214
    {
215
        if (empty($remote_name) || ($remote_name == 'origin')) {
216
            return $this->remote;
217
        }
218
        return Remote::fromDir($this->dir, $remote_name);
219
    }
220
221
    public function url($remote_name = '')
222
    {
223
        return $this->remote($remote_name)->url();
224
    }
225
226
    public function dir()
227
    {
228
        return $this->dir;
229
    }
230
231
    public function valid()
232
    {
233
        return $this->remote->valid();
234
    }
235
236
    public function org($remote_name = '')
237
    {
238
        return $this->remote($remote_name)->org();
239
    }
240
241
    public function project($remote_name = '')
242
    {
243
        return $this->remote($remote_name)->project();
244
    }
245
246
    public function projectWithOrg($remote_name = '')
247
    {
248
        return $this->remote($remote_name)->projectWithOrg();
249
    }
250
251
    /**
252
     * List modified files.
253
     */
254
    public function status()
255
    {
256
        return $this->git('status --porcelain');
257
    }
258
259
    /**
260
     * Fetch from the specified remote.
261
     */
262
    public function fetch($remote, $branch)
263
    {
264
        $this->git('fetch {remote} {branch}', ['remote' => $remote, 'branch' => $branch]);
265
        return $this;
266
    }
267
268
    /**
269
     * Fetch from the specified remote.
270
     */
271
    public function fetchTags($remote = 'origin')
272
    {
273
        $this->fetch($remote, '--tags');
274
        return $this;
275
    }
276
277
    /**
278
     * Pull from the specified remote.
279
     */
280
    public function pull($remote, $branch)
281
    {
282
        $this->git('pull {remote} {branch}', ['remote' => $remote, 'branch' => $branch]);
283
        return $this;
284
    }
285
286
    /**
287
     * Push the specified branch to the desired remote.
288
     */
289
    public function push($remote = '', $branch = '', $force = false)
290
    {
291
        if (empty($remote)) {
292
            $remote = isset($this->fork) ? 'fork' : 'origin';
293
        }
294
        if (empty($branch)) {
295
            $branch = $this->branch();
296
        }
297
        $flag = $force ? '--force ' : '';
298
        $this->git('push {flag}{remote} {branch}', ['remote' => $remote, 'branch' => $branch, 'flag' => $flag]);
299
        return $this;
300
    }
301
302
    /**
303
     * Force-push the branch
304
     */
305
    public function forcePush($remote = '', $branch = '')
306
    {
307
        return $this->push($remote, $branch, true);
308
    }
309
310
    /**
311
     * Merge the specified branch into the current branch.
312
     */
313
    public function merge($branch, $modes = 0)
314
    {
315
        $flags = '';
316
        if ($modes & static::FORCE_MERGE_COMMIT) {
317
            $flags .= ' --no-ff';
318
        }
319
320
        $this->git('merge{flags} {branch}', ['branch' => $branch, 'flags' => $flags]);
321
        return $this;
322
    }
323
324
    public function cherryPick($sha)
325
    {
326
        $this->git('cherry-pick {sha}', ['sha' => $sha]);
327
        return $this;
328
    }
329
330
    /**
331
     * Reset to the specified reference.
332
     */
333
    public function reset($ref = '', $hard = false)
334
    {
335
        $flag = $hard ? '--hard ' : '';
336
        $this->git('reset {flag}{ref}', ['ref' => $ref, 'flag' => $flag]);
337
    }
338
339
    /**
340
     * switchBranch is a synonym for 'checkout'
341
     */
342
    public function switchBranch($branch)
343
    {
344
        $this->git('checkout {branch}', ['branch' => $branch]);
345
        return $this;
346
    }
347
348
    /**
349
     * Switch to the specified branch. Use 'createBranch' to create a new branch.
350
     */
351
    public function checkout($branch)
352
    {
353
        $this->git('checkout {branch}', ['branch' => $branch]);
354
        return $this;
355
    }
356
357
    /**
358
     * Create a new branch
359
     */
360
    public function createBranch($branch, $base = '', $force = false)
361
    {
362
        $flag = $force ? '-B' : '-b';
363
        $this->git('checkout {flag} {branch} {base}', ['branch' => $branch, 'base' => $base, 'flag' => $flag]);
364
        return $this;
365
    }
366
367
    /**
368
     * Stage the items at the specified path.
369
     */
370
    public function add($itemsToAdd)
371
    {
372
        $this->git('add ' . $itemsToAdd);
373
        return $this;
374
    }
375
376
    /**
377
     * Stage everything
378
     */
379
    public function addAll()
380
    {
381
        $this->git('add -A --force .');
382
        return $this;
383
    }
384
385
    /**
386
     * Commit the staged changes.
387
     *
388
     * @param string $message
389
     * @param bool $amend
390
     */
391
    public function commit($message, $amend = false)
392
    {
393
        $flag = $amend ? '--amend ' : '';
394
        $this->git("commit {flag}-m '{message}'", ['message' => $message, 'flag' => $flag]);
395
        return $this;
396
    }
397
398
    /**
399
     * Commit the staged changes by a specified user at specified date.
400
     *
401
     * @param string $message
402
     * @param string $author
403
     * @param string $commit_date
404
     * @param bool $amend
405
     */
406
    public function commitBy($message, $author, $commit_date, $amend = false)
407
    {
408
        $flag = $amend ? '--amend ' : '';
409
        $this->git("commit {flag}-m '{message}' --author='{author}' --date='{date}'", ['message' => $message, 'author' => $author, 'date' => $commit_date, 'flag' => $flag]);
410
        return $this;
411
    }
412
413
    /**
414
     * Ammend the top commit without altering the message.
415
     */
416
    public function amend()
417
    {
418
        return $this->commit($this->message(), true);
419
    }
420
421
    /**
422
     * Add a tag
423
     */
424
    public function tag($tag, $ref = '')
425
    {
426
        $this->git("tag $tag $ref");
427
        return $this;
428
    }
429
430
    /**
431
     * Return the commit message for the sprecified ref
432
     */
433
    public function message($ref = 'HEAD')
434
    {
435
        return trim(implode("\n", $this->git('log --format=%B -n 1 {ref}', ['ref' => $ref])));
436
    }
437
438
    /**
439
     * Return the commit date for the sprecified ref
440
     */
441
    public function commitDate($ref = 'HEAD')
442
    {
443
        return trim(implode("\n", $this->git('log -1 --date=iso --pretty=format:"%cd" {ref}', ['ref' => $ref])));
444
    }
445
446
    public function branch($ref = 'HEAD')
447
    {
448
        return trim(implode("\n", $this->git('rev-parse --abbrev-ref {ref}', ['ref' => $ref])));
449
    }
450
451
    public function revParse($ref)
452
    {
453
        return trim(implode("\n", $this->git('rev-parse {ref}', ['ref' => $ref])));
454
    }
455
456
    /**
457
     * Show a diff of the current modified and uncommitted files.
458
     */
459
    public function diff()
460
    {
461
        return trim(implode("\n", $this->git('diff')));
462
    }
463
464
    /**
465
     * Create a pull request.
466
     *
467
     * @param string $message
468
     * @return $this
469
     */
470
    public function pr($message, $body = '', $base = 'master', $head = '', $forked_org = '')
0 ignored issues
show
Unused Code introduced by
The parameter $forked_org is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
471
    {
472
        if (empty($head)) {
473
            $head = $this->branch();
474
        }
475
        if (isset($this->fork)) {
476
            $forked_org = $this->fork->org();
477
            $head = "$forked_org:$head";
478
        }
479
480
        $this->logger->notice('Create pull request for {org_project} using {head} from {base}', ['org_project' => $this->projectWithOrg(), 'head' => $head, 'base' => $base]);
481
482
        $result = $this->api->prCreate($this->org(), $this->project(), $message, $body, $base, $head);
0 ignored issues
show
Unused Code introduced by
$result 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...
483
484
        return $this;
485
    }
486
487
    /**
488
     * Show a diff of the specified reference from the commit before it.
489
     */
490
    public function show($ref = "HEAD")
491
    {
492
        return implode("\n", $this->git("show $ref"));
493
    }
494
495
    /**
496
     * Add a remote (or change the URL to an existing remote)
497
     */
498
    public function addRemote($url, $remote)
499
    {
500
        return static::setRemoteUrl($url, $this->dir, $remote);
501
    }
502
503
    /**
504
     * If the directory exists, check its remote. Fail if there is
505
     * some project there that is not the requested project.
506
     */
507
    protected function confirmCachedRepoHasCorrectRemote($emptyOk = false)
508
    {
509
        if (!file_exists($this->dir)) {
510
            return;
511
        }
512
        // Check to see if the remote origin is already set to our exact url
513
        $currentURL = exec("git -C {$this->dir} config --get remote.origin.url", $output, $result);
514
515
        if ($currentURL == $this->url()) {
516
            return;
517
        }
518
        // If the API exists, try to repair the URL if the existing URL is close
519
        // (e.g. someone switched authentication tokens)
520
        if ($this->api) {
521
            if (($emptyOk && empty($currentURL)) || ($this->api->addTokenAuthentication($currentURL) == $this->url())) {
522
                static::setRemoteUrl($this->url(), $this->dir);
523
                return;
524
            }
525
        }
526
527
        // TODO: This error message is a potential credentials leak
528
        throw new \Exception("Directory `{$this->dir}` exists and is a clone of `$currentURL` rather than `{$this->url()}`");
529
    }
530
531
    /**
532
     * Set the remote origin to the provided url
533
     * @param string $url
534
     * @param string $dir
535
     * @param string $remote
536
     */
537
    protected static function setRemoteUrl($url, $dir, $remote = 'origin')
538
    {
539
        if (is_dir($dir)) {
540
            $currentURL = exec("git -C {$dir} config --get remote.{$remote}.url");
541
            $gitCommand = empty($currentURL) ? 'add' : 'set-url';
542
            exec("git -C {$dir} remote {$gitCommand} {$remote} {$url}");
543
        }
544
        $remote = new Remote($url);
545
546
        return $remote;
547
    }
548
549
    /**
550
     * If the directory does not exist, then clone it.
551
     */
552
    public function cloneIfNecessary($branch = false, $depth = false)
553
    {
554
        // If the directory exists, we have already validated that it points
555
        // at the correct repository.
556
        if (!is_dir($this->dir)) {
557
            $this->freshClone($branch, $depth);
558
        }
559
        // Make sure that we are on 'master' (or the specified branch) and up-to-date.
560
        $branchTerm = $branch ?: 'master';
561
        exec("git -C '{$this->dir}' reset --hard 2>/dev/null", $output, $result);
562
        exec("git -C '{$this->dir}' checkout $branchTerm 2>/dev/null", $output, $result);
563
        exec("git -C '{$this->dir}' pull origin $branchTerm 2>/dev/null", $output, $result);
564
    }
565
566
    protected function freshClone($branch = false, $depth = false)
567
    {
568
        // Remove $this->dir if it exists, then make sure its parents exist.
569
        $fs = new Filesystem();
570
        if (is_dir($this->dir)) {
571
            $fs->remove($this->dir);
572
        }
573
        $fs->mkdir(dirname($this->dir));
574
575
        $branchTerm = $branch ? "--branch=$branch " : '';
576
        $depthTerm = $depth ? "--depth=$depth " : '';
577
        exec("git clone '{$this->url()}' $branchTerm$depthTerm'{$this->dir}' 2>/dev/null", $output, $result);
578
579
        // Fail if we could not clone.
580
        if ($result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
581
            $project = $this->projectWithOrg();
582
            throw new \Exception("Could not clone $project: git failed with exit code $result");
583
        }
584
    }
585
586
    /**
587
     * Run a git function on the local working copy. Fail on error.
588
     *
589
     * @return string stdout
590
     */
591
    public function git($cmd, $replacements = [], $redacted = [])
592
    {
593
        return $this->execWithRedaction('git {dir}' . $cmd, ['dir' => "-C {$this->dir} "] + $replacements, ['dir' => ''] + $redacted);
594
    }
595
}
596