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 |
||
9 | * @subpackage Composer |
||
10 | * @author Christian Opitz <[email protected]> |
||
11 | * @license http://www.netresearch.de Netresearch Copyright |
||
12 | * @link http://www.netresearch.de |
||
13 | */ |
||
14 | |||
15 | namespace Netresearch\Kite\Workflow\Composer; |
||
16 | use Netresearch\Kite\Service\Composer\Package; |
||
17 | use Netresearch\Kite\Workflow; |
||
18 | use Netresearch\Kite\Exception; |
||
19 | |||
20 | /** |
||
21 | * Abstract for composer workflows |
||
22 | * |
||
23 | * @category Netresearch |
||
24 | * @package Netresearch\Kite\Workflow |
||
25 | * @subpackage Composer |
||
26 | * @author Christian Opitz <[email protected]> |
||
27 | * @license http://www.netresearch.de Netresearch Copyright |
||
28 | * @link http://www.netresearch.de |
||
29 | */ |
||
30 | abstract class Base extends Workflow |
||
31 | { |
||
32 | /** |
||
33 | * @var Package[] |
||
34 | */ |
||
35 | protected $pushPackages = array(); |
||
36 | |||
37 | /** |
||
38 | * @var array |
||
39 | */ |
||
40 | protected $whitelists; |
||
41 | |||
42 | /** |
||
43 | * Configure the variables |
||
44 | * |
||
45 | * @return array |
||
46 | */ |
||
47 | protected function configureVariables() |
||
48 | { |
||
49 | $config = $this->getParent()->get('config'); |
||
50 | if (!array_key_exists('composer', $config)) { |
||
51 | $config['composer'] = []; |
||
52 | } |
||
53 | foreach (['whitelistNames', 'whitelistPaths', 'whitelistRemotes'] as $key) { |
||
54 | if (!array_key_exists($key, $config['composer'])) { |
||
55 | $config['composer'][$key] = null; |
||
56 | } |
||
57 | } |
||
58 | return [ |
||
59 | 'whitelistNames' => array( |
||
60 | 'default' => '{config["composer"]["whitelistNames"]}', |
||
61 | 'type' => 'string', |
||
62 | 'label' => 'Regular expression for package names, to limit this operation to', |
||
63 | 'option' => true |
||
64 | ), |
||
65 | 'whitelistPaths' => array( |
||
66 | 'default' => '{config["composer"]["whitelistPaths"]}', |
||
67 | 'type' => 'string', |
||
68 | 'label' => 'Regular expression for package paths, to limit this operation to', |
||
69 | 'option' => true |
||
70 | ), |
||
71 | 'whitelistRemotes' => array( |
||
72 | 'default' => '{config["composer"]["whitelistRemotes"]}', |
||
73 | 'type' => 'string', |
||
74 | 'label' => 'Regular expression for package remote urls, to limit this operation to', |
||
75 | 'option' => true |
||
76 | ), |
||
77 | '--' |
||
78 | ] + parent::configureVariables(); |
||
79 | } |
||
80 | |||
81 | |||
82 | /** |
||
83 | * Push all packages marked to be pushed |
||
84 | * |
||
85 | * @return void |
||
86 | */ |
||
87 | protected function pushPackages() |
||
88 | { |
||
89 | foreach ($this->pushPackages as $i => $package) { |
||
90 | $this->assertPackageAllowed($package); |
||
91 | $this->console->output("Pushing <comment>$package->name</comment>", false); |
||
92 | $this->git('push', $package->path, array('u' => 'origin', $package->branch)); |
||
93 | $this->console->output( |
||
94 | str_repeat(chr(8), strlen($package->name)) |
||
95 | . '<info>' . $package->name . '</info>' |
||
96 | ); |
||
97 | unset($this->pushPackages[$i]); |
||
98 | } |
||
99 | } |
||
100 | |||
101 | /** |
||
102 | * doComposerUpdateIfNecessary and possible |
||
103 | * |
||
104 | * @return void |
||
105 | */ |
||
106 | protected function doComposerUpdate() |
||
107 | { |
||
108 | try { |
||
109 | $this->composer('update'); |
||
110 | } catch (\Exception $e) { |
||
111 | $this->console->output('<warning>Composer update failed</warning>'); |
||
112 | $this->console->output('<comment>This might have occured because previous pushes to git did not reache the composer repo yet</comment>'); |
||
113 | if ($this->confirm('Retry?')) { |
||
114 | $this->doComposerUpdate(); |
||
115 | } else { |
||
116 | $this->doExit('', 1); |
||
117 | } |
||
118 | } |
||
119 | } |
||
120 | |||
121 | /** |
||
122 | * Go through all packages and check if packages requiring those packages, |
||
123 | * still require their (likely new) versions. |
||
124 | * |
||
125 | * If not and $autoFix or user agrees, the require-statements in the |
||
126 | * dependent packages are changed accordingly. |
||
127 | * |
||
128 | * @param Package[] $packages Packages |
||
129 | * @param bool $autoFix Whether to autofix wrong requirements |
||
130 | * |
||
131 | * @return void |
||
132 | */ |
||
133 | protected function rewriteRequirements(array &$packages, $autoFix = false) |
||
134 | { |
||
135 | $checkedOutPackages = array_keys($packages); |
||
136 | $unfixedRequirements = 0; |
||
137 | while ($packageName = array_shift($checkedOutPackages)) { |
||
138 | $branch = $packages[$packageName]->branch; |
||
139 | $version = 'dev-' . $branch; |
||
140 | foreach ($this->getPackages(false, false) as $package) { |
||
141 | if (array_key_exists($packageName, $package->requires)) { |
||
142 | // TODO: Set required version to branch alias, if any |
||
143 | $requiredVersion = $package->requires[$packageName]; |
||
144 | if ($requiredVersion === '@dev') { |
||
145 | $requiredVersion = 'dev-master'; |
||
146 | } |
||
147 | if ($requiredVersion !== $version) { |
||
148 | $this->assertPackageAllowed($package); |
||
149 | if (!$package->git) { |
||
150 | throw new Exception("Package {$package->name} required to be installed from source"); |
||
151 | } |
||
152 | if ($autoFix) { |
||
153 | $fix = true; |
||
154 | $this->output("Changing required version of {$packageName} in {$package->name} from {$requiredVersion} to {$version}"); |
||
155 | } else { |
||
156 | $this->output("<warning>{$package->name} depends on {$packageName} {$package->requires[$packageName]} and not {$version} as expected</warning>"); |
||
157 | $this->output('<comment>If you don\'t fix that, the branches will probably change with composer update</comment>'); |
||
158 | $fix = $this->confirm('Fix that?'); |
||
159 | } |
||
160 | if ($fix) { |
||
161 | if (!array_key_exists($package->name, $packages)) { |
||
162 | $this->checkoutPackage($package, $branch, true); |
||
163 | $checkedOutPackages[] = $package->name; |
||
164 | $packages[$package->name] = $package; |
||
165 | } |
||
166 | |||
167 | $this->pushPackages[$packageName] = $packages[$packageName]; |
||
168 | $this->rewriteRequirement($package, $packageName, $version); |
||
169 | } else { |
||
170 | $unfixedRequirements++; |
||
171 | } |
||
172 | } |
||
173 | } |
||
174 | } |
||
175 | } |
||
176 | if ($unfixedRequirements) { |
||
177 | $this->doExit( |
||
178 | 'It seems like a composer update is required but due to probably incorrect dependencies you have to do that manually', 1 |
||
179 | ); |
||
180 | } |
||
181 | } |
||
182 | |||
183 | /** |
||
184 | * Change the require statement for $requiredPackage to the newVersion in $package |
||
185 | * |
||
186 | * @param Package $package The package |
||
187 | * @param string $requiredPackage The required package |
||
188 | * @param string $newVersion The new version |
||
189 | * |
||
190 | * @return void |
||
191 | */ |
||
192 | protected function rewriteRequirement($package, $requiredPackage, $newVersion) |
||
193 | { |
||
194 | $this->assertPackageAllowed($package); |
||
195 | |||
196 | $currentVersion = $package->requires[$requiredPackage]; |
||
197 | $composerFile = $package->path . '/composer.json'; |
||
198 | $composerFileContents = file_get_contents($composerFile); |
||
199 | $newComposerFileContents = preg_replace( |
||
200 | sprintf( |
||
201 | '/(^\s*"require"\s*:\s*\{[^\}]+"%s"\s*:\s*")%s/m', |
||
202 | preg_quote($requiredPackage, '/'), |
||
203 | preg_quote($currentVersion, '/') |
||
204 | ), |
||
205 | '$1' . $newVersion, |
||
206 | $composerFileContents |
||
207 | ); |
||
208 | file_put_contents($composerFile, $newComposerFileContents); |
||
209 | $this->reloadRequires($package); |
||
0 ignored issues
–
show
|
|||
210 | if ($package->requires[$requiredPackage] !== $newVersion) { |
||
211 | file_put_contents($composerFile, $composerFileContents); |
||
212 | $this->output('<error>Could not replace version</error> - generated composer.json was:'); |
||
213 | $this->output($newComposerFileContents); |
||
214 | throw new Exception('Replacing version failed'); |
||
215 | } |
||
216 | |||
217 | $this->git('commit', $package->path, array('n' => true, 'm' => "Change required version of $requiredPackage to $newVersion", 'composer.json')); |
||
218 | if (!isset($package->source)) { |
||
219 | $package->source = new \stdClass(); |
||
220 | } |
||
221 | $package->source->reference = $this->git('rev-parse', $package->path, array('HEAD')); |
||
222 | |||
223 | $this->console->output("Made <comment>$package->name</comment> require <comment>$requiredPackage $newVersion</comment>"); |
||
224 | |||
225 | $this->pushPackages[$package->name] = $package; |
||
226 | } |
||
227 | |||
228 | /** |
||
229 | * Reload the requires from $package composer.json to $package->requires |
||
230 | * |
||
231 | * If $detectChanges, and there are changes on the requirements not in $ignorePackages |
||
232 | * composer update is requested |
||
233 | * |
||
234 | * @param Package $package The package |
||
235 | * |
||
236 | * @deprecated Use $package->reloadRequires() |
||
237 | * |
||
238 | * @return void |
||
239 | */ |
||
240 | protected function reloadRequires($package) |
||
241 | { |
||
242 | $package->reloadRequires(); |
||
243 | } |
||
244 | |||
245 | /** |
||
246 | * Checkout a package at a branch |
||
247 | * |
||
248 | * @param Package $package The package |
||
249 | * @param string $branch The branch |
||
250 | * @param bool $create Create the branch if it doesn't exist |
||
251 | * |
||
252 | * @return bool|null Whether checkout was successful or null when package is already at this branch |
||
253 | */ |
||
254 | protected function checkoutPackage($package, $branch, $create = false) |
||
255 | { |
||
256 | $this->assertPackageAllowed($package); |
||
257 | |||
258 | if ($package->branch === $branch) { |
||
259 | return null; |
||
260 | } |
||
261 | if (!$package->git) { |
||
262 | throw new Exception('Non git package can not be checked out'); |
||
263 | } |
||
264 | $remoteBranch = 'origin/' . $branch; |
||
265 | $isRemote = in_array($remoteBranch, $package->branches, true); |
||
266 | if (in_array($branch, $package->branches, true)) { |
||
267 | $this->git('checkout', $package->path, $branch); |
||
268 | } elseif ($isRemote) { |
||
269 | $this->git('checkout', $package->path, array('b' => $branch, $remoteBranch)); |
||
270 | } elseif ($create) { |
||
271 | $branches = array_unique( |
||
272 | array_map( |
||
273 | function ($el) { |
||
274 | $parts = explode('/', $el); |
||
275 | return array_pop($parts); |
||
276 | }, |
||
277 | $package->branches |
||
278 | ) |
||
279 | ); |
||
280 | sort($branches); |
||
281 | $inferFromBranch = $this->choose( |
||
282 | "Select branch to create new branch '$branch' from in {$package->name}", |
||
283 | $branches, in_array('master', $branches, true) ? 'master' : $branch |
||
284 | ); |
||
285 | if ($inferFromBranch !== $package->branch) { |
||
286 | $this->checkoutPackage($package, $inferFromBranch); |
||
287 | } |
||
288 | $this->git('checkout', $package->path, array('b' => $branch)); |
||
289 | $package->branches[] = $branch; |
||
290 | } else { |
||
291 | return false; |
||
292 | } |
||
293 | |||
294 | if ($isRemote) { |
||
295 | if (!isset($package->upstreams[$branch]) || $package->upstreams[$branch] !== $remoteBranch) { |
||
296 | $this->git('branch', $package->path, array('u' => $remoteBranch)); |
||
297 | $package->upstreams[$branch] = $remoteBranch; |
||
298 | } |
||
299 | $this->git('rebase', $package->path); |
||
300 | } |
||
301 | |||
302 | $this->console->output("Checked out <comment>{$package->name}</comment> at <comment>$branch</comment>"); |
||
303 | |||
304 | $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...
|
|||
305 | $package->version = 'dev-' . $branch; |
||
306 | $package->branch = $branch; |
||
307 | |||
308 | return true; |
||
309 | } |
||
310 | |||
311 | /** |
||
312 | * Merge a $branch into $package's current branch |
||
313 | * |
||
314 | * @param Package $package The package |
||
315 | * @param string $branch The branch |
||
316 | * @param null $message The commit message (if any) |
||
317 | * @param bool $squash Whether to squash the changes |
||
318 | * |
||
319 | * @return void |
||
320 | */ |
||
321 | protected function mergePackage($package, $branch, $message = null, $squash = false) |
||
322 | { |
||
323 | $this->assertPackageAllowed($package); |
||
324 | |||
325 | if (!$package->git) { |
||
326 | throw new Exception('Non git package can not be merged'); |
||
327 | } |
||
328 | |||
329 | $this->git('fetch', $package->path, array('force' => true, 'origin', $branch . ':' . $branch)); |
||
330 | |||
331 | $ff = $branch == 'master' ? 'ff' : 'no-ff'; |
||
332 | $optArg = array($ff => true, 'no-commit' => true); |
||
333 | if ($squash) { |
||
334 | $optArg['squash'] = true; |
||
335 | } |
||
336 | $optArg[] = $branch; |
||
337 | |||
338 | try { |
||
339 | $this->git('merge', $package->path, $optArg); |
||
340 | } catch (\Exception $e) { |
||
341 | $diff = $this->git('diff', $package->path, array('name-only' => true, 'diff-filter' => 'U')); |
||
342 | $conflictedFiles = array_flip(explode("\n", $diff)); |
||
343 | if (array_key_exists('composer.json', $conflictedFiles)) { |
||
344 | try { |
||
345 | $this->resolveRequirementsConflict($package); |
||
346 | $this->git('add', $package->path, 'composer.json'); |
||
347 | } catch (Exception $conflictSolvingException) { |
||
348 | } |
||
349 | } |
||
350 | if (array_diff(array_keys($conflictedFiles), ['composer.json'])) { |
||
351 | throw new Exception( |
||
352 | 'There are unresolved conflicts - please resolve them and then commit the result', |
||
353 | 1458307785, isset($conflictSolvingException) ? $conflictSolvingException : null |
||
354 | ); |
||
355 | } elseif (isset($conflictSolvingException)) { |
||
356 | throw $conflictSolvingException; |
||
357 | } |
||
358 | } |
||
359 | if (isset($conflictedFiles) || $this->git('status', $package->path, array('porcelain' => true))) { |
||
360 | if (!$message) { |
||
361 | $message = $this->answer( |
||
362 | 'Enter commit message:', |
||
363 | 'Merged ' . $branch . ' into ' . $package->branch |
||
364 | ); |
||
365 | } |
||
366 | $this->git('commit', $package->path, array('n' => true, 'm' => $message)); |
||
367 | } |
||
368 | |||
369 | $this->console->output("Merged with <comment>$branch</comment> in <comment>{$package->name}</comment>"); |
||
370 | |||
371 | $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...
|
|||
372 | $this->pushPackages[$package->name] = $package; |
||
373 | } |
||
374 | |||
375 | /** |
||
376 | * Try to solve conflicts inside the require section of the $package composer.json |
||
377 | * |
||
378 | * @param Package $package The package |
||
379 | * |
||
380 | * @return void |
||
381 | */ |
||
382 | private function resolveRequirementsConflict($package) |
||
383 | { |
||
384 | $contents = file_get_contents($package->path . '/composer.json'); |
||
385 | $ours = @json_decode( |
||
386 | preg_replace('/^<{7}.+\n(.+)\n(\|{7}|={7}).+>{7}.+$/smU', '$1', $contents) |
||
387 | ); |
||
388 | $theirs = @json_decode( |
||
389 | preg_replace('/^<{7}.+\n={7}\n(.+)\n>{7}.+$/smU', '$1', $contents) |
||
390 | ); |
||
391 | if (!is_object($ours) || !is_object($theirs)) { |
||
392 | throw new Exception('Could not regenerate json file from solved conflicts'); |
||
393 | } |
||
394 | $diff = array_diff_key($theirs = get_object_vars($theirs), $ours = get_object_vars($ours)); |
||
395 | foreach ($ours as $key => $value) { |
||
396 | if ($key !== 'require' && (!array_key_exists($key, $theirs) || serialize($value) !== serialize($theirs[$key]))) { |
||
397 | $diff[$key] = $value; |
||
398 | } |
||
399 | } |
||
400 | if ($diff !== array()) { |
||
401 | throw new Exception('Can not automerge composer.json due to conflicts outside require object', 1458307516); |
||
402 | } |
||
403 | |||
404 | if (preg_match('/\{[^\{]*<{7}.+?>{7}[^\{]*([\t ]*)\}/smU', $contents, $matches, PREG_OFFSET_CAPTURE)) { |
||
405 | $prefix = "\n" . str_repeat($matches[1][0], 2); |
||
406 | $requireBlock = ''; |
||
407 | foreach ($this->mergeRequirements($package, $ours, $theirs) as $packageName => $version) { |
||
408 | $requireBlock .= $prefix . '"' . $packageName . '": "' . $version . '",'; |
||
409 | } |
||
410 | file_put_contents( |
||
411 | $package->path . '/composer.json', |
||
412 | substr($contents, 0, $matches[0][1]) . '{' |
||
413 | . rtrim($requireBlock, ',') . "\n" |
||
414 | . $matches[1][0] . '}' |
||
415 | . substr($contents, $matches[0][1] + strlen($matches[0][0])) |
||
416 | ); |
||
417 | } |
||
418 | } |
||
419 | |||
420 | /** |
||
421 | * Merge the requirements from different sides of the current $package |
||
422 | * |
||
423 | * @param Package $package The current package |
||
424 | * @param array $ours Our composer.json as array |
||
425 | * @param array $theirs Their composer.json as array |
||
426 | * |
||
427 | * @return array |
||
428 | */ |
||
429 | private function mergeRequirements($package, array $ours, array $theirs) |
||
430 | { |
||
431 | $oursRequire = isset($ours['require']) && is_object($ours['require']) ? get_object_vars($ours['require']) : []; |
||
432 | $theirsRequire = isset($theirs['require']) && is_object($theirs['require']) ? get_object_vars($theirs['require']) : []; |
||
433 | $mergedRequires = array_merge($oursRequire, $theirsRequire); |
||
434 | $packages = $this->getPackages(false, false); |
||
435 | $preferredVersion = 'dev-' . $package->branch; |
||
436 | foreach ($mergedRequires as $packageName => $version) { |
||
437 | $actualVersion = ($version === '@dev') ? 'dev-master' : $version; |
||
438 | if (array_key_exists($packageName, $oursRequire) |
||
439 | && $version !== $oursRequire[$packageName] |
||
440 | && $actualVersion !== $oursRequire[$packageName] |
||
441 | && $actualVersion !== $preferredVersion |
||
442 | && array_key_exists($packageName, $packages) |
||
443 | && in_array($package->branch, $packages[$packageName]->branches, true) |
||
444 | ) { |
||
445 | $mergedRequires[$packageName] = $preferredVersion; |
||
446 | } |
||
447 | } |
||
448 | return $mergedRequires; |
||
449 | } |
||
450 | |||
451 | /** |
||
452 | * Get the allowed packages |
||
453 | * |
||
454 | * @param bool $gitOnly If git packages should be returned only |
||
455 | * @param bool $allowedOnly If allowed packages should be returned only |
||
456 | * |
||
457 | * @return \Netresearch\Kite\Service\Composer\Package[] |
||
458 | */ |
||
459 | protected function getPackages($gitOnly = true, $allowedOnly = true) |
||
460 | { |
||
461 | /* @var $packages \Netresearch\Kite\Service\Composer\Package[] */ |
||
462 | /* @var $package \Netresearch\Kite\Service\Composer\Package */ |
||
463 | $packages = array(); |
||
464 | foreach ($this->get('composer.packages') as $package) { |
||
465 | if ((!$gitOnly || $package->git) && (!$allowedOnly || $this->isPackageAllowed($package))) { |
||
466 | $packages[$package->name] = $package; |
||
467 | } |
||
468 | } |
||
469 | return $packages; |
||
470 | } |
||
471 | |||
472 | /** |
||
473 | * Assert that package is allowed |
||
474 | * |
||
475 | * @param Package $package The package |
||
476 | * |
||
477 | * @throws Exception |
||
478 | * |
||
479 | * @return void |
||
480 | */ |
||
481 | protected function assertPackageAllowed($package) |
||
482 | { |
||
483 | if (!$this->isPackageAllowed($package)) { |
||
484 | throw new Exception("Package {$package->name} is not in white list"); |
||
485 | } |
||
486 | } |
||
487 | |||
488 | /** |
||
489 | * Determine if a package is whitelisted |
||
490 | * |
||
491 | * @param Package $package The package |
||
492 | * |
||
493 | * @return bool |
||
494 | */ |
||
495 | protected function isPackageAllowed(Package $package) |
||
496 | { |
||
497 | $whitelistTypes = ['path', 'remote', 'name']; |
||
498 | if (!is_array($this->whitelists)) { |
||
499 | $this->whitelists = []; |
||
500 | foreach ($whitelistTypes as $whitelistType) { |
||
501 | $option = $this->get('whitelist' . ucfirst($whitelistType) . 's'); |
||
502 | if ($option) { |
||
503 | $this->whitelists[$whitelistType] = '#^' . $option . '$#'; |
||
504 | } |
||
505 | } |
||
506 | } |
||
507 | |||
508 | if (!$this->whitelists) { |
||
509 | // Without whitelists, all packages are allowed |
||
510 | return true; |
||
511 | } |
||
512 | |||
513 | foreach ($this->whitelists as $type => $pattern) { |
||
514 | $subject = $package->$type; |
||
515 | if ($type === 'path') { |
||
516 | $subject = rtrim( |
||
517 | $this->console->getFilesystem()->findShortestPath( |
||
518 | $this->get('composer.rootPackage.path'), |
||
519 | $subject, |
||
520 | true |
||
521 | ), |
||
522 | '/' |
||
523 | ); |
||
524 | } |
||
525 | if (preg_match($pattern, $subject)) { |
||
526 | return true; |
||
527 | } |
||
528 | } |
||
529 | |||
530 | return false; |
||
531 | } |
||
532 | } |
||
533 | ?> |
||
534 |
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.