These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * See class comment |
||
4 | * |
||
5 | * PHP Version 5 |
||
6 | * |
||
7 | * @category Netresearch |
||
8 | * @package Netresearch\Kite\Workflow\Composer |
||
9 | * @author Christian Opitz <[email protected]> |
||
10 | * @license http://www.netresearch.de Netresearch Copyright |
||
11 | * @link http://www.netresearch.de |
||
12 | */ |
||
13 | |||
14 | namespace Netresearch\Kite\Workflow\Composer; |
||
15 | use Netresearch\Kite\Exception; |
||
16 | |||
17 | use Netresearch\Kite\Workflow; |
||
18 | |||
19 | /** |
||
20 | * Workflow to diagnose packages and fix found problems |
||
21 | * |
||
22 | * @category Netresearch |
||
23 | * @package Netresearch\Kite\Workflow\Composer |
||
24 | * @author Christian Opitz <[email protected]> |
||
25 | * @license http://www.netresearch.de Netresearch Copyright |
||
26 | * @link http://www.netresearch.de |
||
27 | */ |
||
28 | class Diagnose extends Base |
||
29 | { |
||
30 | /** |
||
31 | * @var array |
||
32 | */ |
||
33 | protected $checks = array(); |
||
34 | |||
35 | /** |
||
36 | * @var array |
||
37 | */ |
||
38 | protected $fixes = array(); |
||
39 | |||
40 | /** |
||
41 | * @var bool |
||
42 | */ |
||
43 | protected $composerUpdateRequired = false; |
||
44 | |||
45 | /** |
||
46 | * @var bool |
||
47 | */ |
||
48 | protected $dontCheckCurrentPackageAgain = false; |
||
49 | |||
50 | /** |
||
51 | * Configure the options |
||
52 | * |
||
53 | * @return array |
||
54 | */ |
||
55 | protected function configureVariables() |
||
56 | { |
||
57 | foreach (get_class_methods($this) as $method) { |
||
58 | if (substr($method, 0, 5) === 'check' && $method[5] === strtoupper($method[5])) { |
||
59 | $check = substr($method, 5); |
||
60 | $this->checks[] = $check; |
||
61 | if (method_exists($this, 'fix' . $check)) { |
||
62 | $this->fixes[] = $check; |
||
63 | } |
||
64 | } |
||
65 | } |
||
66 | |||
67 | return array( |
||
68 | 'check' => array( |
||
69 | 'type' => 'array', |
||
70 | 'option' => true, |
||
71 | 'label' => 'Only execute these checks - available checks are ' . implode(', ', $this->checks), |
||
72 | ), |
||
73 | 'fix' => array( |
||
74 | 'type' => 'boolean|array', |
||
75 | 'option' => true, |
||
76 | 'label' => 'Enable fixes and optionally reduce to certain fixes - available fixes are ' . implode(', ', $this->fixes), |
||
77 | ), |
||
78 | ) + parent::configureVariables(); |
||
79 | } |
||
80 | |||
81 | /** |
||
82 | * Override to assemble the tasks |
||
83 | * |
||
84 | * @return void |
||
85 | */ |
||
86 | public function assemble() |
||
87 | { |
||
88 | $this->callback( |
||
89 | function () { |
||
90 | $fix = $this->get('fix'); |
||
91 | $fixes = ($fix === true) ? $this->fixes : (array) $fix; |
||
92 | $checks = $this->get('check') ?: $this->checks; |
||
93 | foreach ($checks as $check) { |
||
94 | $this->doCheck($check, in_array($check, $fixes, true)); |
||
95 | } |
||
96 | } |
||
97 | ); |
||
98 | } |
||
99 | |||
100 | /** |
||
101 | * Run the checks |
||
102 | * |
||
103 | * @param string $check The check to execute |
||
104 | * @param boolean $fix Whether to fix found problems |
||
105 | * @param array $packageNames Package names to filter this one |
||
106 | * (for recursive calls only) |
||
107 | * |
||
108 | * @return void |
||
109 | */ |
||
110 | public function doCheck($check, $fix, array $packageNames = array()) |
||
111 | { |
||
112 | $packages = $this->get('composer.packages'); |
||
113 | $errors = 0; |
||
114 | if (!$packageNames) { |
||
115 | $this->console->output($check, false); |
||
116 | $this->console->indent(); |
||
117 | } |
||
118 | $rerunForPackages = array(); |
||
119 | foreach ($packages as $package) { |
||
120 | if ($packageNames && !in_array($package->name, $packageNames, true)) { |
||
121 | continue; |
||
122 | } |
||
123 | if (is_string($message = $this->{'check' . $check}($package))) { |
||
124 | View Code Duplication | if (!$packageNames && !$errors) { |
|
125 | $this->console->output( |
||
126 | str_repeat(chr(8), strlen($check)) |
||
127 | . '<fg=red;bg=black>' . $check . '</>' |
||
128 | ); |
||
129 | } |
||
130 | $message = sprintf($message, "package <comment>$package->name</>"); |
||
131 | $this->console->output(ucfirst($message)); |
||
132 | $errors++; |
||
133 | if ($fix) { |
||
134 | $this->dontCheckCurrentPackageAgain = false; |
||
135 | $this->console->indent(); |
||
136 | $this->{'fix' . $check}($package); |
||
137 | $this->console->outdent(); |
||
138 | if (!$this->dontCheckCurrentPackageAgain) { |
||
139 | $rerunForPackages[] = $package->name; |
||
140 | } |
||
141 | } |
||
142 | } |
||
143 | $this->pushPackages(); |
||
144 | if ($this->composerUpdateRequired) { |
||
145 | $this->composerUpdateRequired = false; |
||
146 | $this->doComposerUpdate(); |
||
147 | $this->doCheck($check, $fix); |
||
148 | return; |
||
149 | } |
||
150 | } |
||
151 | if ($rerunForPackages) { |
||
152 | $this->doCheck($check, $fix, $rerunForPackages); |
||
153 | } |
||
154 | View Code Duplication | if (!$packageNames) { |
|
155 | if (!$errors) { |
||
156 | $this->console->output( |
||
157 | str_repeat(chr(8), strlen($check)) |
||
158 | . '<fg=green;bg=black>' . $check . '</>' |
||
159 | ); |
||
160 | } |
||
161 | $this->console->outdent(); |
||
162 | } |
||
163 | } |
||
164 | |||
165 | /** |
||
166 | * Check for unstaged changes |
||
167 | * |
||
168 | * @param object $package The package |
||
169 | * |
||
170 | * @return null|string message string on problems, null otherwise |
||
171 | */ |
||
172 | protected function checkUnstagedChanges($package) |
||
173 | { |
||
174 | if ($package->git) { |
||
175 | $status = $this->git('status', $package->path, array('porcelain' => true)); |
||
176 | if (trim($status)) { |
||
177 | return '%s has uncommited changes'; |
||
178 | } |
||
179 | } |
||
180 | return null; |
||
181 | } |
||
182 | |||
183 | /** |
||
184 | * Fix unstaged changes |
||
185 | * |
||
186 | * @param object $package The package |
||
187 | * |
||
188 | * @return void |
||
189 | */ |
||
190 | protected function fixUnstagedChanges($package) |
||
191 | { |
||
192 | $fix = $this->selectFixes( |
||
193 | array( |
||
194 | 1 => 'Show diff (and ask again)', |
||
195 | 2 => 'Withdraw changes', |
||
196 | 3 => 'Stash changes', |
||
197 | ) |
||
198 | ); |
||
199 | switch ($fix) { |
||
200 | case 1: |
||
201 | $this->git('add', $package->path, array('N' => true, 'A' => true)); |
||
202 | $this->console->output(''); |
||
203 | $this->git('diff', $package->path, 'HEAD', array('tty' => true)); |
||
204 | $this->console->output(''); |
||
205 | $this->fixUnstagedChanges($package); |
||
206 | break; |
||
207 | case 2: |
||
208 | $this->git('reset', $package->path, array('hard' => true)); |
||
209 | $this->git('clean', $package->path, array('i' => true, 'd' => true), array('tty' => true)); |
||
210 | break; |
||
211 | case 3: |
||
212 | $this->git('reset', $package->path); |
||
213 | $args = 'save -u'; |
||
214 | if ($message = $this->answer('Message for stash:')) { |
||
215 | $args .= ' ' . escapeshellarg($message); |
||
216 | } |
||
217 | $this->git('stash', $package->path, $args); |
||
218 | break; |
||
219 | } |
||
220 | } |
||
221 | |||
222 | /** |
||
223 | * Check if package is ahead and/or behind remote tracking branch |
||
224 | * |
||
225 | * @param object $package The package |
||
226 | * |
||
227 | * @return null|string message string on problems, null otherwise |
||
228 | */ |
||
229 | protected function checkRemoteSynchronicity($package) |
||
230 | { |
||
231 | if ($package->git) { |
||
232 | $status = $this->git('status', $package->path, '--branch --porcelain -u no'); |
||
233 | list($branchInfo) = explode("\n", $status, 2); |
||
234 | preg_match('/ \[(ahead|behind) [0-9]+(?:, (ahead|behind) [0-9]+)?\]$/', $branchInfo, $matches); |
||
235 | array_shift($matches); |
||
236 | $package->ahead = in_array('ahead', $matches, true); |
||
237 | $package->behind = in_array('behind', $matches, true); |
||
238 | if ($package->ahead && $package->behind) { |
||
239 | $type = 'has diverged from'; |
||
240 | } elseif ($package->ahead) { |
||
241 | $type = 'is ahead of'; |
||
242 | } elseif ($package->behind) { |
||
243 | $type = 'is behind'; |
||
244 | } else { |
||
245 | return null; |
||
246 | } |
||
247 | return "%s $type remote tracking branch"; |
||
248 | } |
||
249 | return null; |
||
250 | } |
||
251 | |||
252 | /** |
||
253 | * Push and/or pull or show the differences |
||
254 | * |
||
255 | * @param object $package The package |
||
256 | * |
||
257 | * @return void |
||
258 | */ |
||
259 | protected function fixRemoteSynchronicity($package) |
||
260 | { |
||
261 | $commands = array(); |
||
262 | if ($package->behind) { |
||
263 | $commands[] = 'pull'; |
||
264 | } |
||
265 | if ($package->ahead) { |
||
266 | $commands[] = 'push'; |
||
267 | } |
||
268 | $fixes = array( |
||
269 | 1 => 'Show incoming/outgoing commits (and ask again)', |
||
270 | 2 => ucfirst(implode(' and ', $commands)), |
||
271 | ); |
||
272 | if (count($commands) > 1 && $package->branch !== 'master') { |
||
273 | $fixes[3] = 'Push with <comment>--force</comment>'; |
||
274 | } |
||
275 | switch ($this->selectFixes($fixes)) { |
||
276 | case 1: |
||
277 | $this->gitRevDiff($package, '@\{u\}', 'Remote', 'Local'); |
||
278 | $this->fixRemoteSynchronicity($package); |
||
279 | break; |
||
280 | case 3: |
||
281 | $commands = array('push'); |
||
282 | $options = '--force'; |
||
283 | case 2: |
||
284 | foreach ($commands as $command) { |
||
285 | $pck = "<comment>{$package->name}</comment>"; |
||
286 | $this->console->output($msg = ucfirst($command) . "ing $pck...", false); |
||
287 | $this->git($command, $package->path, isset($options) ? $options : null); |
||
288 | $this->console->output( |
||
289 | str_repeat(chr(8), strlen(strip_tags($msg))) |
||
290 | . "<fg=green>Sucessfully {$command}ed $pck</>" |
||
291 | ); |
||
292 | } |
||
293 | break; |
||
294 | } |
||
295 | } |
||
296 | |||
297 | /** |
||
298 | * Checks for packages that require the package in another branch than the |
||
299 | * current - or for packages that require the package in a version when |
||
300 | * the package is checked out at a branch |
||
301 | * |
||
302 | * Further requirements checks are left to composer |
||
303 | * |
||
304 | * @param object $package The package |
||
305 | * |
||
306 | * @return null|string message string on problems, null otherwise |
||
307 | */ |
||
308 | protected function checkRequirementsMatch($package) |
||
309 | { |
||
310 | if ($package->git && !$package->isRoot) { |
||
311 | $package->requiredBranch = null; |
||
312 | $package->unsatisfiedDependentPackages = array(); |
||
313 | $package->invalidRequirements = false; |
||
314 | foreach ($this->get('composer.packages') as $dependentPackage) { |
||
315 | if (!isset($dependentPackage->requiresUpToDate)) { |
||
316 | $this->reloadRequires($dependentPackage); |
||
0 ignored issues
–
show
|
|||
317 | $dependentPackage->requiresUpToDate = true; |
||
318 | } |
||
319 | if (array_key_exists($package->name, $dependentPackage->requires)) { |
||
320 | if (substr($dependentPackage->requires[$package->name], 0, 4) === 'dev-') { |
||
321 | $requiredBranch = substr($dependentPackage->requires[$package->name], 4); |
||
322 | if (strpos($requiredBranch, '#')) { |
||
323 | $otherHash = isset($hash) ? $hash : null; |
||
324 | list($requiredBranch, $hash) = explode('#', $requiredBranch); |
||
325 | if ($otherHash && $otherHash !== $hash) { |
||
326 | return '<error>Two or more packages require %s in different commits</error>'; |
||
327 | } |
||
328 | } |
||
329 | if ($package->requiredBranch && $package->requiredBranch !== $requiredBranch) { |
||
330 | $package->invalidRequirements = true; |
||
331 | return '<error>Two or more packages require %s in different branches</error>'; |
||
332 | } |
||
333 | $package->requiredBranch = $requiredBranch; |
||
334 | if ($requiredBranch !== $package->branch) { |
||
335 | $package->unsatisfiedDependentPackages[$package->name] = $dependentPackage; |
||
336 | } |
||
337 | } elseif ($package->branch) { |
||
338 | $package->unsatisfiedDependentPackages[$package->name] = $dependentPackage; |
||
339 | } |
||
340 | } |
||
341 | } |
||
342 | $constraint = $package->tag ?: 'dev-' . $package->branch; |
||
343 | if ($package->requiredBranch && $package->requiredBranch !== $package->branch) { |
||
344 | return "%s is at <comment>$constraint</comment> but is required at <comment>dev-{$package->requiredBranch}</comment>"; |
||
345 | } |
||
346 | if (isset($hash) && substr($package->source->reference, 0, strlen($hash)) !== $hash) { |
||
347 | return "%s is at <comment>{$constraint}#{$package->source->reference}</comment> but is required at <comment>dev-{$package->requiredBranch}#{$hash}</comment>"; |
||
348 | } |
||
349 | } |
||
350 | return null; |
||
351 | } |
||
352 | |||
353 | /** |
||
354 | * Checkout package at required branch or make dependent packages require the |
||
355 | * current branch of the package. |
||
356 | * |
||
357 | * @param object $package The package |
||
358 | * |
||
359 | * @return void |
||
360 | */ |
||
361 | protected function fixRequirementsMatch($package) |
||
362 | { |
||
363 | if ($package->invalidRequirements) { |
||
364 | $this->doExit('Can not fix that', 1); |
||
365 | } |
||
366 | $currentConstraint = $package->tag ?: 'dev-' . $package->branch; |
||
367 | $requiredConstraint = 'dev-' . $package->requiredBranch; |
||
368 | if ($package->requiredBranch) { |
||
369 | $actions = array( |
||
370 | 1 => "Show divergent commits between <comment>$currentConstraint</comment> and <comment>$requiredConstraint</comment> (and ask again)", |
||
371 | 2 => "Checkout package at <comment>$requiredConstraint</comment>" |
||
372 | ); |
||
373 | } else { |
||
374 | $actions = array(); |
||
375 | } |
||
376 | if ($count = count($package->unsatisfiedDependentPackages)) { |
||
377 | $actions[3] = "Make "; |
||
378 | $git = true; |
||
379 | foreach ($package->unsatisfiedDependentPackages as $i => $requirePackage) { |
||
380 | $git &= $requirePackage->git; |
||
381 | if ($i === $count - 1 && $i > 0) { |
||
382 | $actions[3] .= 'and '; |
||
383 | } |
||
384 | $actions[3] .= "<comment>{$requirePackage->name}</comment> "; |
||
385 | } |
||
386 | $actions[3] .= "require <comment>{$currentConstraint}</comment>"; |
||
387 | if (!$git) { |
||
388 | unset($actions[3]); |
||
389 | } |
||
390 | } |
||
391 | switch ($this->selectFixes($actions)) { |
||
392 | case 1: |
||
393 | $this->gitRevDiff($package, $package->requiredBranch, $requiredConstraint, $currentConstraint); |
||
394 | $this->fixRequirementsMatch($package); |
||
395 | break; |
||
396 | case 2: |
||
397 | $this->checkoutPackage($package, $package->requiredBranch); |
||
398 | $this->reloadRequires($package); |
||
0 ignored issues
–
show
The method
Netresearch\Kite\Workflo...\Base::reloadRequires() has been deprecated with message: Use $package->reloadRequires()
This method has been deprecated. The supplier of the class has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.
Loading history...
|
|||
399 | break; |
||
400 | case 3: |
||
401 | foreach ($package->unsatisfiedDependentPackages as $dependentPackage) { |
||
402 | $this->rewriteRequirement($dependentPackage, $package->name, $currentConstraint); |
||
403 | } |
||
404 | break; |
||
405 | } |
||
406 | } |
||
407 | |||
408 | /** |
||
409 | * Check if package is on another branch/tag or another commit than locked |
||
410 | * |
||
411 | * @param object $package The package |
||
412 | * |
||
413 | * @return null|string message string on problems, null otherwise |
||
414 | */ |
||
415 | protected function checkDivergeFromLock($package) |
||
416 | { |
||
417 | if ($package->git && !$package->isRoot) { |
||
418 | $constraint = $package->tag ? ltrim($package->tag, 'v') : 'dev-' . $package->branch; |
||
419 | if (($package->tag || $package->branch) && $package->version !== $constraint) { |
||
420 | if ($this->git('rev-parse', $package->path, 'HEAD') === $package->source->reference) { |
||
421 | // HEAD is tip of branch and tag - so only branch was detected |
||
422 | return; |
||
423 | } |
||
424 | return "%s is at <comment>$constraint</comment> but is locked at <comment>{$package->version}</comment>"; |
||
425 | } |
||
426 | $rawCounts = $this->git('rev-list', $package->path, "--count --left-right --cherry-pick {$package->source->reference}..."); |
||
427 | $counts = explode("\t", $rawCounts); |
||
428 | if ($counts[0] || $counts[1]) { |
||
429 | $num = $counts[0] ?: $counts[1]; |
||
430 | $type = $counts[0] ? 'behind</>' : 'ahead</> of'; |
||
431 | return '%s is <comment>' . $num . ' commit' . ($num > 1 ? 's ' : ' ') |
||
432 | . $type . ' locked commit <comment>' |
||
433 | . substr($package->source->reference, 0, 7) . '</>'; |
||
434 | } |
||
435 | } |
||
436 | return null; |
||
437 | } |
||
438 | |||
439 | /** |
||
440 | * Run composer update or checkout the package at locked state |
||
441 | * |
||
442 | * @param object $package The package |
||
443 | * |
||
444 | * @return void |
||
445 | */ |
||
446 | protected function fixDivergeFromLock($package) |
||
447 | { |
||
448 | $fix = $this->selectFixes( |
||
449 | array( |
||
450 | 1 => 'Show commits between locked and current commit (and ask again)', |
||
451 | 2 => 'Run <info>composer update</info> (you may loose local changes)', |
||
452 | 3 => 'Checkout package at locked commit <comment>' |
||
453 | . substr($package->source->reference, 0, 7) |
||
454 | . "</comment> (<comment>{$package->version}</comment>)", |
||
455 | ) |
||
456 | ); |
||
457 | |||
458 | switch ($fix) { |
||
459 | case 1: |
||
460 | $this->gitRevDiff($package, $package->source->reference, 'Locked', 'Current'); |
||
461 | $this->fixDivergeFromLock($package); |
||
462 | break; |
||
463 | case 2: |
||
464 | $this->composerUpdateRequired = true; |
||
465 | break; |
||
466 | case 3: |
||
467 | $this->git('checkout', $package->path, $package->source->reference); |
||
468 | $this->reloadRequires($package); |
||
0 ignored issues
–
show
The method
Netresearch\Kite\Workflo...\Base::reloadRequires() has been deprecated with message: Use $package->reloadRequires()
This method has been deprecated. The supplier of the class has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.
Loading history...
|
|||
469 | break; |
||
470 | } |
||
471 | } |
||
472 | |||
473 | /** |
||
474 | * Check if composer lock is up to date |
||
475 | * |
||
476 | * @param object $package The package |
||
477 | * |
||
478 | * @return null|string message string on problems, null otherwise |
||
479 | */ |
||
480 | protected function checkComposerLockActuality($package) |
||
481 | { |
||
482 | if ($package->isRoot) { |
||
483 | $lock = json_decode(file_get_contents('composer.lock')); |
||
484 | if (md5_file('composer.json') !== $lock->{'hash'}) { |
||
485 | return 'The lock file is not up to date with the latest changes in root composer.json'; |
||
486 | } |
||
487 | } |
||
488 | return null; |
||
489 | } |
||
490 | |||
491 | /** |
||
492 | * Run composer update (--lock) |
||
493 | * |
||
494 | * @param object $package The package |
||
495 | * |
||
496 | * @return void |
||
497 | */ |
||
498 | protected function fixComposerLockActuality($package) |
||
499 | { |
||
500 | $fix = $this->selectFixes( |
||
501 | array( |
||
502 | 1 => 'Run <info>composer update</info> (you may loose local changes)', |
||
503 | ) |
||
504 | ); |
||
505 | if ($fix === 1) { |
||
506 | $this->composerUpdateRequired = true; |
||
507 | } |
||
508 | } |
||
509 | |||
510 | /** |
||
511 | * Select from the available fixes |
||
512 | * |
||
513 | * @param array $fixes The fixes |
||
514 | * |
||
515 | * @return int|null |
||
516 | */ |
||
517 | protected function selectFixes($fixes) |
||
518 | { |
||
519 | $this->console->outdent(); |
||
520 | $fixes['n'] = 'Nothing for now, ask again later'; |
||
521 | $fixes['i'] = 'Ignore'; |
||
522 | $fixes['x'] = 'Exit'; |
||
523 | $result = $this->choose('What do you want to do?', $fixes); |
||
524 | $this->console->indent(); |
||
525 | if ($result == 'i' || $result == 'n') { |
||
526 | $this->dontCheckCurrentPackageAgain = ($result === 'i'); |
||
527 | return null; |
||
528 | } |
||
529 | if ($result == 'x') { |
||
530 | $this->doExit(); |
||
531 | } |
||
532 | return (int) $result; |
||
533 | } |
||
534 | |||
535 | /** |
||
536 | * Show commits that are not in the HEAD but in ref and vice versa |
||
537 | * |
||
538 | * @param object $package The package |
||
539 | * @param string $ref The ref name |
||
540 | * @param string $leftTitle The title of the ref name |
||
541 | * @param string $rightTitle The title of HEAD |
||
542 | * |
||
543 | * @return void |
||
544 | */ |
||
545 | protected function gitRevDiff($package, $ref, $leftTitle, $rightTitle) |
||
546 | { |
||
547 | for ($i = 0; $i <= 1; $i++) { |
||
548 | $side = $i ? 'left' : 'right'; |
||
549 | $otherSide = $i ? 'right' : 'left'; |
||
550 | $args = "--$side-only --cherry-pick --pretty=format:'%C(yellow)%h %Cgreen%cd %an%Creset%n %s' --abbrev-commit --date=local "; |
||
551 | $args .= $ref . '...'; |
||
552 | $log = $this->git('log', $package->path, $args, array('shy' => true)); |
||
553 | if ($log) { |
||
554 | $this->console->output(''); |
||
555 | |||
556 | $title = "<info>{${$side . 'Title'}}</info> > <info>{${$otherSide . 'Title'}}</info>"; |
||
557 | $this->output($title); |
||
558 | $this->console->output(str_repeat('-', strlen(strip_tags($title)))); |
||
559 | |||
560 | $this->console->output($log); |
||
561 | |||
562 | $this->console->output(''); |
||
563 | } |
||
564 | } |
||
565 | } |
||
566 | } |
||
567 | ?> |
||
568 |
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.