Completed
Push — master ( 53b99e...05e097 )
by Basil
03:48
created

RepoController::getCloneUrlBasedOnType()   A

Complexity

Conditions 2
Paths 2

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 2
nc 2
nop 2
1
<?php
2
3
namespace luya\dev;
4
5
use Curl\Curl;
6
use GitWrapper\GitWrapper;
7
use yii\console\widgets\Table;
8
use yii\console\Markdown;
9
use yii\helpers\Console;
10
use luya\helpers\FileHelper;
11
use Nadar\PhpComposerReader\ComposerReader;
12
use Nadar\PhpComposerReader\AutoloadSection;
13
use Nadar\PhpComposerReader\Autoload;
14
15
/**
16
 * Dev Env cloning and updating.
17
 *
18
 * Provdes functions to clone and update the repos.
19
 *
20
 * Usage
21
 *
22
 * ```sh
23
 * ./vendor/bin/luyadev repo/init
24
 * ./vendor/bin/luyadev repo/update
25
 * ```
26
 *
27
 * Or clone a custom repo into the repos folder:
28
 *
29
 * ```sh
30
 * ./venodr/bin/luyadev repo/clone luya-module-news luyadev
31
 * ```
32
 *
33
 * In order to remove an existing repo from update list
34
 *
35
 * ```sh
36
 * ./vendor/bin/luyadev repo/remove luya-module-news
37
 * ```
38
 *
39
 * @author Basil Suter <[email protected]>
40
 * @since 1.0.1
41
 */
42
class RepoController extends BaseDevCommand
43
{
44
    const CONFIG_VAR_USERNAME = 'username';
45
    
46
    const CONFIG_VAR_CLONETYPE = 'cloneType';
47
    
48
    const CONFIG_VAR_CUSTOMCLONES = 'customClones';
49
    
50
    /**
51
     * @var string Default action is actionInit();
52
     */
53
    public $defaultAction = 'init';
54
    
55
    /**
56
     * @var array The default repos from luyadev
57
     */
58
    public $repos = [
59
        'luya',
60
        'luya-module-admin',
61
        'luya-module-cms',
62
    ];
63
    
64
    public $text = <<<EOT
65
**CLONE REPOS**
66
67
We've detected that you don't have all module repos forked to your account. You can only push changes to the forked repos, all others are **READ ONLY**.
68
69
If you want to work on a specific repo, make sure that repo is forked to your Github account.
70
71
You can also skip this command, fork the repos and rerun this command again.
72
73
**FORK ME**
74
EOT;
75
    
76
    /**
77
     * Initilize the main repos.
78
     *
79
     * @return number
80
     */
81
    public function actionInit()
82
    {
83
        // username
84
        $username = $this->getConfig(self::CONFIG_VAR_USERNAME);
85
        if (!$username) {
86
            $username = $this->prompt('Whats your Github username?');
87
            $this->saveConfig(self::CONFIG_VAR_USERNAME, $username);
88
        }
89
        
90
        // clonetype
91
        $cloneType = $this->getConfig(self::CONFIG_VAR_CLONETYPE);
92
        if (!$cloneType) {
93
            $cloneType = $this->select('Are you connected via ssh or https?', ['ssh' => 'ssh', 'http' => 'http']);
94
            $this->saveConfig(self::CONFIG_VAR_CLONETYPE, $cloneType);
95
        }
96
        
97
        $summary = [];
98
        $itemWithoutFork = false;
99
        
100
        // generate summary overview
101
        foreach ($this->repos as $repo) {
102
            $newRepoHome = $this->getFilesystemRepoPath($repo);
103
            if (file_exists($newRepoHome . DIRECTORY_SEPARATOR . '.git')) {
104
                $summary[] = $this->summaryItem($repo, false, true);
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...
Documentation introduced by
true 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...
105
            } elseif ($this->forkExists($username, $repo)) {
0 ignored issues
show
Bug introduced by
It seems like $username defined by $this->getConfig(self::CONFIG_VAR_USERNAME) on line 84 can also be of type boolean; however, luya\dev\RepoController::forkExists() does only seem to accept string, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
106
                $summary[] = $this->summaryItem($repo, true, false);
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...
Documentation introduced by
true 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...
107
            } else {
108
                $itemWithoutFork = true;
109
                $summary[] = $this->summaryItem($repo, false, false);
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...
110
            }
111
        }
112
        
113
        if ($itemWithoutFork) {
114
            Console::clearScreen();
115
            $this->outputInfo($this->markdown($this->text));
116
            foreach ($summary as $sum) {
117
                if (!$sum[2] && !$sum[1]) {
118
                    $this->outputInfo($this->markdown("**{$sum[0]}**: https://github.com/luyadev/{$sum[0]}/fork", true));
119
                }
120
            }
121
            echo (new Table())->setHeaders(['Repo', 'Already initialized', 'Fork exists'])->setRows($summary)->run();
122
            $this->outputError("Repos without fork detected. Those repos will be initialized as READ ONLY. It means you can not push any changes to them.");
123
            
124
            if (!$this->confirm("Continue?")) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm('Continue?') of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
125
                return $this->outputError('Abort by User.');
126
            }
127
        }
128
        
129
        // foreach summary and clone
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
130
        foreach ($summary as $sum) {
131
            $repo = $sum[0];
132
            $hasFork = $sum[2];
133
            $exists = $sum[1];
134
            
135
            // continue already initialized repos.
136
            if ($exists) {
137
                continue;
138
            }
139
            
140
            $newRepoHome = $this->getFilesystemRepoPath($repo);
141
            
142
            if ($hasFork) {
143
                $cloneUrl = ($cloneType == 'ssh') ? "[email protected]:{$username}/{$repo}.git" : "https://github.com/{$username}/{$repo}.git";
144
            } else {
145
                $cloneUrl = ($cloneType == 'ssh') ? "[email protected]:luyadev/{$repo}.git" : "https://github.com/{$username}/{$repo}.git";
146
            }
147
            
148
            $this->cloneRepo($repo, $cloneUrl, $newRepoHome, 'luyadev');
149
        }
150
        
151
        return $this->outputSuccess("init complete.");
152
    }
153
    
154
    /**
155
     * Update all repos to master branch from upstream.
156
     */
157
    public function actionUpdate()
158
    {
159
        foreach ($this->repos as $repo) {
160
            $this->rebaseRepo($repo, $this->getFilesystemRepoPath($repo));
161
        }
162
        
163
        foreach ($this->getConfig(self::CONFIG_VAR_CUSTOMCLONES, []) as $repo => $path) {
0 ignored issues
show
Bug introduced by
The expression $this->getConfig(self::C..._CUSTOMCLONES, array()) of type boolean is not traversable.
Loading history...
164
            $this->rebaseRepo($repo, $path);
165
        }
166
    }
167
    
168
    /**
169
     * Clone a repo into the repos folder.
170
     *
171
     * @param string $repo
172
     * @param string $vendor
173
     */
174
    public function actionClone($vendor = null, $repo = null)
175
    {
176
        // if `vendor/repo` notation is provided
177
        if ($vendor !== null && strpos($vendor, '/')) {
178
            list($vendor, $repo) = explode("/", $vendor);
179
        }
180
        
181
        if (empty($vendor)) {
182
            $vendor = $this->prompt("Enter the username/vendor for this repo (e.g. luyadev)");
183
        }
184
        
185
        if (empty($repo)) {
186
            $repo = $this->prompt("Enter the name of the repo you like to clone (e.g. luya-module-news)");
187
        }
188
        
189
        $clones = $this->getConfig(self::CONFIG_VAR_CUSTOMCLONES, []);
190
        
191
        $repoFileSystemPath = $this->getFilesystemRepoPath($repo);
192
        
193
        $clones[$repo] = $repoFileSystemPath;
194
        
195
        $this->cloneRepo($repo, $this->getCloneUrlBasedOnType($repo, $vendor), $repoFileSystemPath, $vendor);
196
        
197
        $this->saveConfig(self::CONFIG_VAR_CUSTOMCLONES, $clones);
198
        
199
        $composerReader = new ComposerReader($repoFileSystemPath . DIRECTORY_SEPARATOR . 'composer.json');
200
        
201
        if ($composerReader->canRead()) {
202
            $section = new AutoloadSection($composerReader);
203
            $autoloaders = [];
204
            foreach ($section as $autoload) {
205
                $newSrc = $repoFileSystemPath . DIRECTORY_SEPARATOR . $autoload->source;
206
                $autoloaders[] = ['autoload' => $autoload, 'src' => $newSrc];
207
            }
208
            
209
            if (!empty($autoloaders)) {
210
                foreach ($autoloaders as $item) {
211
                    $projectComposer = $this->getProjectComposerReader();
212
                    if ($projectComposer->canWrite()) {
213
                        $new = new Autoload($projectComposer, $item['autoload']->namespace, $item['src'], $item['autoload']->type);
214
                        
215
                        $section = new AutoloadSection($projectComposer);
216
                        $section->add($new)->save();
217
                        
218
                        $this->outputSuccess("{$repo}: autoload ✔ (namespace '{$item['autoload']->namespace}' for '{$item['autoload']->source}')");
219
                    }
220
                }
221
                
222
                $projectComposer = $this->getProjectComposerReader();
223
                $projectComposer->runCommand('dump-autoload');
224
            }
225
        }
226
    }
227
    
228
    /**
229
     * Remove a given repo from filesystem.
230
     *
231
     * @param string $repo The repo name like `luya-module-cms` without vendor.
232
     */
233
    public function actionRemove($repo)
234
    {
235
        FileHelper::removeDirectory($this->getFilesystemRepoPath($repo));
236
        $clones = $this->getConfig(self::CONFIG_VAR_CUSTOMCLONES, []);
237
        if (isset($clones[$repo])) {
238
            unset($clones[$repo]);
239
            $this->saveConfig(self::CONFIG_VAR_CUSTOMCLONES, $clones);
240
        }
241
        
242
        return $this->outputSuccess("Removed repo {$repo}.");
243
    }
244
    
245
    /**
246
     *
247
     * @return \Nadar\PhpComposerReader\ComposerReader
248
     */
249
    protected function getProjectComposerReader()
250
    {
251
        return new ComposerReader(getcwd() . DIRECTORY_SEPARATOR . 'composer.json');
252
    }
253
    
254
    private $_gitWrapper;
255
    
256
    /**
257
     * @return \GitWrapper\GitWrapper
258
     */
259
    protected function getGitWrapper()
260
    {
261
        if ($this->_gitWrapper === null) {
262
            $this->_gitWrapper = new GitWrapper();
263
            $this->_gitWrapper->setTimeout(300);
264
        }
265
    
266
        return $this->_gitWrapper;
267
    }
268
    
269
    /**
270
     *
271
     * @param string $repo
272
     * @param string $isFork
273
     * @param string $exists
274
     * @return array
275
     */
276
    private function summaryItem($repo, $isFork, $exists)
277
    {
278
        return [$repo, $exists, $isFork];
279
    }
280
    
281
    /**
282
     *
283
     * @param string $repo
284
     * @return string
285
     */
286
    private function getFilesystemRepoPath($repo)
287
    {
288
        return 'repos' . DIRECTORY_SEPARATOR . $repo;
289
    }
290
    
291
    /**
292
     *
293
     * @param string $username
294
     * @param string $repo
295
     * @return boolean
296
     */
297
    private function forkExists($username, $repo)
298
    {
299
        return (new Curl())->get('https://api.github.com/repos/'.$username.'/'.$repo)->isSuccess();
300
    }
301
    
302
    /**
303
     *
304
     * @param string $text
305
     * @param boolean $paragraph
306
     * @return string
307
     */
308
    private function markdown($text, $paragraph = false)
309
    {
310
        $parser = new Markdown();
311
    
312
        if ($paragraph) {
313
            return $parser->parseParagraph($text);
314
        }
315
    
316
        return $parser->parse($text);
317
    }
318
    
319
    /**
320
     * Return the url to clone based on config clone type (ssh/https).
321
     *
322
     * @param string $repo
323
     * @param string $username
324
     * @return string
325
     */
326
    private function getCloneUrlBasedOnType($repo, $username)
327
    {
328
        return ($this->getConfig(self::CONFIG_VAR_CLONETYPE) == 'ssh') ? "[email protected]:{$username}/{$repo}.git" : "https://github.com/{$username}/{$repo}.git";
329
    }
330
    
331
    /**
332
     * Rebase existing repo.
333
     *
334
     * @param string $repo
335
     * @param string $repoFileSystemPath
336
     */
337
    private function rebaseRepo($repo, $repoFileSystemPath)
338
    {
339
        $wrapper = new GitWrapper();
340
        try {
341
            $wrapper->git('fetch upstream', $repoFileSystemPath);
342
            $this->outputInfo("{$repo}: fetch upstream ✔");
343
            
344
            $wrapper->git('checkout master', $repoFileSystemPath);
345
            $this->outputInfo("{$repo}: checkout master ✔");
346
            
347
            $wrapper->git('rebase upstream/master master', $repoFileSystemPath);
348
            $this->outputInfo("{$repo}: rebase master ✔");
349
            
350
            $wrapper->git('pull', $repoFileSystemPath);
351
            $this->outputInfo("{$repo}: pull ✔");
352
        } catch (\Exception $err) {
353
            $this->outputError("{$repo}: error while updating ({$repoFileSystemPath}) with message: " . $err->getMessage());
354
        }
355
    }
356
    
357
    /**
358
     * Clone a repo into the repos folder.
359
     *
360
     * @param string $repo
361
     * @param string $cloneUrl
362
     * @param string $newRepoHome
363
     * @param string $upstreamUsername The upstream vendor name of the repo if available.
364
     */
365
    private function cloneRepo($repo, $cloneUrl, $newRepoHome, $upstreamUsername)
366
    {
367
        $this->outputSuccess("{$repo}: cloning {$cloneUrl} ...");
368
        $this->getGitWrapper()->cloneRepository($cloneUrl, $newRepoHome);
369
        
370
        if (!empty($upstreamUsername)) {
371
            $this->getGitWrapper()->git('remote add upstream https://github.com/'.$upstreamUsername.'/'.$repo.'.git', $newRepoHome);
372
            $this->outputInfo("{$repo}: Configure upstream https://github.com/{$upstreamUsername}/{$repo}.git ✔");
373
        }
374
        
375
        $this->outputSuccess("{$repo}: cloning ✔");
376
    }
377
}
378