Completed
Push — master ( 4e834f...b4ab2f )
by Basil
23:26
created

RepoController::actionClone()   C

Complexity

Conditions 10
Paths 72

Size

Total Lines 50
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 50
rs 5.7647
c 0
b 0
f 0
cc 10
eloc 27
nc 72
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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("added psr4 entry {$item['autoload']->namespace}");
219
                    }
220
                }
221
            }
222
        }
223
    }
224
    
225
    /**
226
     * Remove a given repo from filesystem.
227
     * 
228
     * @param string $repo The repo name like `luya-module-cms` without vendor.
229
     */
230
    public function actionRemove($repo)
231
    {
232
    	FileHelper::removeDirectory($this->getFilesystemRepoPath($repo));
233
    	$clones = $this->getConfig(self::CONFIG_VAR_CUSTOMCLONES, []);
234
    	if (isset($clones[$repo])) {
235
	    	unset($clones[$repo]);
236
	    	$this->saveConfig(self::CONFIG_VAR_CUSTOMCLONES, $clones);
237
    	}
238
    	
239
    	return $this->outputSuccess("Removed repo {$repo}.");
240
    }
241
    
242
    /**
243
     * 
244
     * @return \Nadar\PhpComposerReader\ComposerReader
245
     */
246
    protected function getProjectComposerReader()
247
    {
248
        return new ComposerReader(getcwd() . DIRECTORY_SEPARATOR . 'composer.json');
249
    }
250
    
251
    private $_gitWrapper;
252
    
253
    /**
254
     * @return \GitWrapper\GitWrapper
255
     */
256
    protected function getGitWrapper()
257
    {
258
        if ($this->_gitWrapper === null) {
259
            $this->_gitWrapper = new GitWrapper();
260
            $this->_gitWrapper->setTimeout(300);
261
        }
262
    
263
        return $this->_gitWrapper;
264
    }
265
    
266
    /**
267
     * 
268
     * @param string $repo
269
     * @param string $isFork
270
     * @param string $exists
271
     * @return array
272
     */
273
    private function summaryItem($repo, $isFork, $exists)
274
    {
275
        return [$repo, $exists, $isFork];
276
    }
277
    
278
    /**
279
     * 
280
     * @param string $repo
281
     * @return string
282
     */
283
    private function getFilesystemRepoPath($repo)
284
    {
285
        return 'repos' . DIRECTORY_SEPARATOR . $repo;
286
    }
287
    
288
    /**
289
     * 
290
     * @param string $username
291
     * @param string $repo
292
     * @return boolean
293
     */
294
    private function forkExists($username, $repo)
295
    {
296
        return (new Curl())->get('https://api.github.com/repos/'.$username.'/'.$repo)->isSuccess();
297
    }
298
    
299
    /**
300
     * 
301
     * @param string $text
302
     * @param boolean $paragraph
303
     * @return string
304
     */
305
    private function markdown($text, $paragraph = false)
306
    {
307
        $parser = new Markdown();
308
    
309
        if ($paragraph) {
310
            return $parser->parseParagraph($text);
311
        }
312
    
313
        return $parser->parse($text);
314
    }
315
    
316
    /**
317
     * Return the url to clone based on config clone type (ssh/https).
318
     *
319
     * @param string $repo
320
     * @param string $username
321
     * @return string
322
     */
323
    private function getCloneUrlBasedOnType($repo, $username)
324
    {
325
        return ($this->getConfig(self::CONFIG_VAR_CLONETYPE) == 'ssh') ? "[email protected]:{$username}/{$repo}.git" : "https://github.com/{$username}/{$repo}.git";
326
    }
327
    
328
    /**
329
     * Rebase existing repo.
330
     * 
331
     * @param string $repo
332
     * @param string $repoFileSystemPath
333
     */
334
    private function rebaseRepo($repo, $repoFileSystemPath)
335
    {
336
    	$wrapper = new GitWrapper();
337
    	 
338
    	$wrapper->git('checkout master', $repoFileSystemPath);
339
    	$this->outputInfo("{$repo}: checkout master ✔");
340
    	 
341
    	$wrapper->git('fetch upstream', $repoFileSystemPath);
342
    	$this->outputInfo("{$repo}: fetch upstream ✔");
343
    	 
344
    	$wrapper->git('rebase upstream/master master', $repoFileSystemPath);
345
    	$this->outputInfo("{$repo}: rebase master ✔");
346
    }
347
    
348
    /**
349
     * Clone a repo into the repos folder.
350
     *
351
     * @param string $repo
352
     * @param string $cloneUrl
353
     * @param string $newRepoHome
354
     * @param string $upstreamUsername The upstream vendor name of the repo if available.
355
     */
356
    private function cloneRepo($repo, $cloneUrl, $newRepoHome, $upstreamUsername)
357
    {
358
        $this->outputSuccess("{$repo}: cloning {$cloneUrl} ...");
359
        $this->getGitWrapper()->cloneRepository($cloneUrl, $newRepoHome);
360
        
361
        if (!empty($upstreamUsername)) {
362
            $this->getGitWrapper()->git('remote add upstream https://github.com/'.$upstreamUsername.'/'.$repo.'.git', $newRepoHome);
363
            $this->outputInfo("Configure upstream https://github.com/{$upstreamUsername}/{$repo}.git");
364
        }
365
        
366
        $this->outputSuccess("{$repo}: ✔ complete");
367
    }
368
}
369