Completed
Push — develop ( f11ef2...d41b65 )
by David
06:01 queued 11s
created

Patches   F

Complexity

Total Complexity 92

Size/Duplication

Total Lines 531
Duplicated Lines 3.77 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
dl 20
loc 531
rs 2
c 0
b 0
f 0
wmc 92
lcom 1
cbo 1

13 Methods

Rating   Name   Duplication   Size   Complexity  
A activate() 0 8 1
A getSubscribedEvents() 0 16 1
F checkPatches() 10 59 21
C gatherPatches() 10 61 16
B grabPatches() 0 49 11
B postInstall() 0 53 7
A getPackageFromOperation() 0 13 3
B getAndApplyPatch() 0 57 9
A isPatchingEnabled() 0 12 5
A writePatchReport() 0 9 2
A executeCommand() 0 26 5
A arrayMergeRecursiveDistinct() 0 14 5
B applyPatchWithGit() 0 30 6

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Patches often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Patches, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @file
5
 * Provides a way to patch Composer packages after installation.
6
 */
7
8
namespace cweagans\Composer;
9
10
use Composer\Composer;
11
use Composer\DependencyResolver\Operation\InstallOperation;
12
use Composer\DependencyResolver\Operation\UninstallOperation;
13
use Composer\DependencyResolver\Operation\UpdateOperation;
14
use Composer\DependencyResolver\Operation\OperationInterface;
15
use Composer\EventDispatcher\EventSubscriberInterface;
16
use Composer\IO\IOInterface;
17
use Composer\Package\AliasPackage;
18
use Composer\Package\PackageInterface;
19
use Composer\Plugin\PluginInterface;
20
use Composer\Installer\PackageEvents;
21
use Composer\Script\Event;
22
use Composer\Script\ScriptEvents;
23
use Composer\Installer\PackageEvent;
24
use Composer\Util\ProcessExecutor;
25
use Composer\Util\RemoteFilesystem;
26
use Symfony\Component\Process\Process;
27
28
class Patches implements PluginInterface, EventSubscriberInterface {
29
30
  /**
31
   * @var Composer $composer
32
   */
33
  protected $composer;
34
  /**
35
   * @var IOInterface $io
36
   */
37
  protected $io;
38
  /**
39
   * @var EventDispatcher $eventDispatcher
40
   */
41
  protected $eventDispatcher;
42
  /**
43
   * @var ProcessExecutor $executor
44
   */
45
  protected $executor;
46
  /**
47
   * @var array $patches
48
   */
49
  protected $patches;
50
51
  /**
52
   * Apply plugin modifications to composer
53
   *
54
   * @param Composer    $composer
55
   * @param IOInterface $io
56
   */
57
  public function activate(Composer $composer, IOInterface $io) {
58
    $this->composer = $composer;
59
    $this->io = $io;
60
    $this->eventDispatcher = $composer->getEventDispatcher();
61
    $this->executor = new ProcessExecutor($this->io);
62
    $this->patches = array();
63
    $this->installedPatches = array();
64
  }
65
66
  /**
67
   * Returns an array of event names this subscriber wants to listen to.
68
   */
69
  public static function getSubscribedEvents() {
70
    return array(
71
      ScriptEvents::PRE_INSTALL_CMD => array('checkPatches'),
72
      ScriptEvents::PRE_UPDATE_CMD => array('checkPatches'),
73
      PackageEvents::PRE_PACKAGE_INSTALL => array('gatherPatches'),
74
      PackageEvents::PRE_PACKAGE_UPDATE => array('gatherPatches'),
75
      // The following is a higher weight for compatibility with
76
      // https://github.com/AydinHassan/magento-core-composer-installer and more generally for compatibility with
77
      // every Composer plugin which deploys downloaded packages to other locations.
78
      // In such cases you want that those plugins deploy patched files so they have to run after
79
      // the "composer-patches" plugin.
80
      // @see: https://github.com/cweagans/composer-patches/pull/153
81
      PackageEvents::POST_PACKAGE_INSTALL => array('postInstall', 10),
82
      PackageEvents::POST_PACKAGE_UPDATE => array('postInstall', 10),
83
    );
84
  }
85
86
  /**
87
   * Before running composer install,
88
   * @param Event $event
89
   */
90
  public function checkPatches(Event $event) {
91
    if (!$this->isPatchingEnabled()) {
92
      return;
93
    }
94
95
    try {
96
      $repositoryManager = $this->composer->getRepositoryManager();
97
      $localRepository = $repositoryManager->getLocalRepository();
98
      $installationManager = $this->composer->getInstallationManager();
99
      $packages = $localRepository->getPackages();
100
101
      $extra = $this->composer->getPackage()->getExtra();
102
      $patches_ignore = isset($extra['patches-ignore']) ? $extra['patches-ignore'] : array();
103
104
      $tmp_patches = $this->grabPatches();
105
      foreach ($packages as $package) {
106
        $extra = $package->getExtra();
107 View Code Duplication
        if (isset($extra['patches'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
108
          if (isset($patches_ignore[$package->getName()])) {
109
            foreach ($patches_ignore[$package->getName()] as $package_name => $patches) {
110
              if (isset($extra['patches'][$package_name])) {
111
                $extra['patches'][$package_name] = array_diff($extra['patches'][$package_name], $patches);
112
              }
113
            }
114
          }
115
          $this->installedPatches[$package->getName()] = $extra['patches'];
116
        }
117
        $patches = isset($extra['patches']) ? $extra['patches'] : array();
118
        $tmp_patches = $this->arrayMergeRecursiveDistinct($tmp_patches, $patches);
119
      }
120
121
      if ($tmp_patches == FALSE) {
122
        $this->io->write('<info>No patches supplied.</info>');
123
        return;
124
      }
125
126
      // Remove packages for which the patch set has changed.
127
      foreach ($packages as $package) {
128
        if (!($package instanceof AliasPackage)) {
0 ignored issues
show
Bug introduced by
The class Composer\Package\AliasPackage does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
129
          $package_name = $package->getName();
130
          $extra = $package->getExtra();
131
          $has_patches = isset($tmp_patches[$package_name]);
132
          $has_applied_patches = isset($extra['patches_applied']) && count($extra['patches_applied']) > 0;
133
          if (($has_patches && !$has_applied_patches)
134
            || (!$has_patches && $has_applied_patches)
135
            || ($has_patches && $has_applied_patches && $tmp_patches[$package_name] !== $extra['patches_applied'])) {
136
            $uninstallOperation = new UninstallOperation($package, 'Removing package so it can be re-installed and re-patched.');
137
            $this->io->write('<info>Removing package ' . $package_name . ' so that it can be re-installed and re-patched.</info>');
138
            $installationManager->uninstall($localRepository, $uninstallOperation);
139
          }
140
        }
141
      }
142
    }
143
    // If the Locker isn't available, then we don't need to do this.
144
    // It's the first time packages have been installed.
145
    catch (\LogicException $e) {
146
      return;
147
    }
148
  }
149
150
  /**
151
   * Gather patches from dependencies and store them for later use.
152
   *
153
   * @param PackageEvent $event
154
   */
155
  public function gatherPatches(PackageEvent $event) {
156
    // If we've already done this, then don't do it again.
157
    if (isset($this->patches['_patchesGathered'])) {
158
      $this->io->write('<info>Patches already gathered. Skipping</info>', TRUE, IOInterface::VERBOSE);
159
      return;
160
    }
161
    // If patching has been disabled, bail out here.
162
    elseif (!$this->isPatchingEnabled()) {
163
      $this->io->write('<info>Patching is disabled. Skipping.</info>', TRUE, IOInterface::VERBOSE);
164
      return;
165
    }
166
167
    $this->patches = $this->grabPatches();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->grabPatches() of type object<cweagans\Composer\Patches> is incompatible with the declared type array of property $patches.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
168
    if (empty($this->patches)) {
169
      $this->io->write('<info>No patches supplied.</info>');
170
    }
171
172
    $extra = $this->composer->getPackage()->getExtra();
173
    $patches_ignore = isset($extra['patches-ignore']) ? $extra['patches-ignore'] : array();
174
175
    // Now add all the patches from dependencies that will be installed.
176
    $operations = $event->getOperations();
177
    $this->io->write('<info>Gathering patches for dependencies. This might take a minute.</info>');
178
    foreach ($operations as $operation) {
179
      if ($operation->getJobType() == 'install' || $operation->getJobType() == 'update') {
180
        $package = $this->getPackageFromOperation($operation);
181
        $extra = $package->getExtra();
182 View Code Duplication
        if (isset($extra['patches'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
183
          if (isset($patches_ignore[$package->getName()])) {
184
            foreach ($patches_ignore[$package->getName()] as $package_name => $patches) {
185
              if (isset($extra['patches'][$package_name])) {
186
                $extra['patches'][$package_name] = array_diff($extra['patches'][$package_name], $patches);
187
              }
188
            }
189
          }
190
          $this->patches = $this->arrayMergeRecursiveDistinct($this->patches, $extra['patches']);
191
        }
192
        // Unset installed patches for this package
193
        if(isset($this->installedPatches[$package->getName()])) {
194
          unset($this->installedPatches[$package->getName()]);
195
        }
196
      }
197
    }
198
199
    // Merge installed patches from dependencies that did not receive an update.
200
    foreach ($this->installedPatches as $patches) {
201
      $this->patches = $this->arrayMergeRecursiveDistinct($this->patches, $patches);
202
    }
203
204
    // If we're in verbose mode, list the projects we're going to patch.
205
    if ($this->io->isVerbose()) {
206
      foreach ($this->patches as $package => $patches) {
207
        $number = count($patches);
208
        $this->io->write('<info>Found ' . $number . ' patches for ' . $package . '.</info>');
209
      }
210
    }
211
212
    // Make sure we don't gather patches again. Extra keys in $this->patches
213
    // won't hurt anything, so we'll just stash it there.
214
    $this->patches['_patchesGathered'] = TRUE;
215
  }
216
217
  /**
218
   * Get the patches from root composer or external file
219
   * @return Patches
220
   * @throws \Exception
221
   */
222
  public function grabPatches() {
223
      // First, try to get the patches from the root composer.json.
224
    $extra = $this->composer->getPackage()->getExtra();
225
    if (isset($extra['patches'])) {
226
      $this->io->write('<info>Gathering patches for root package.</info>');
227
      $patches = $extra['patches'];
228
      return $patches;
229
    }
230
    // If it's not specified there, look for a patches-file definition.
231
    elseif (isset($extra['patches-file'])) {
232
      $this->io->write('<info>Gathering patches from patch file.</info>');
233
      $patches = file_get_contents($extra['patches-file']);
234
      $patches = json_decode($patches, TRUE);
235
      $error = json_last_error();
236
      if ($error != 0) {
237
        switch ($error) {
238
          case JSON_ERROR_DEPTH:
239
            $msg = ' - Maximum stack depth exceeded';
240
            break;
241
          case JSON_ERROR_STATE_MISMATCH:
242
            $msg =  ' - Underflow or the modes mismatch';
243
            break;
244
          case JSON_ERROR_CTRL_CHAR:
245
            $msg = ' - Unexpected control character found';
246
            break;
247
          case JSON_ERROR_SYNTAX:
248
            $msg =  ' - Syntax error, malformed JSON';
249
            break;
250
          case JSON_ERROR_UTF8:
251
            $msg =  ' - Malformed UTF-8 characters, possibly incorrectly encoded';
252
            break;
253
          default:
254
            $msg =  ' - Unknown error';
255
            break;
256
          }
257
          throw new \Exception('There was an error in the supplied patches file:' . $msg);
258
        }
259
      if (isset($patches['patches'])) {
260
        $patches = $patches['patches'];
261
        return $patches;
262
      }
263
      elseif(!$patches) {
264
        throw new \Exception('There was an error in the supplied patch file');
265
      }
266
    }
267
    else {
268
      return array();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array(); (array) is incompatible with the return type documented by cweagans\Composer\Patches::grabPatches of type cweagans\Composer\Patches.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
269
    }
270
  }
271
272
  /**
273
   * @param PackageEvent $event
274
   * @throws \Exception
275
   */
276
  public function postInstall(PackageEvent $event) {
277
278
    // Check if we should exit in failure.
279
    $extra = $this->composer->getPackage()->getExtra();
280
    $exitOnFailure = getenv('COMPOSER_EXIT_ON_PATCH_FAILURE') || !empty($extra['composer-exit-on-patch-failure']);
281
282
    // Get the package object for the current operation.
283
    $operation = $event->getOperation();
284
    /** @var PackageInterface $package */
285
    $package = $this->getPackageFromOperation($operation);
286
    $package_name = $package->getName();
287
288
    if (!isset($this->patches[$package_name])) {
289
      if ($this->io->isVerbose()) {
290
        $this->io->write('<info>No patches found for ' . $package_name . '.</info>');
291
      }
292
      return;
293
    }
294
    $this->io->write('  - Applying patches for <info>' . $package_name . '</info>');
295
296
    // Get the install path from the package object.
297
    $manager = $event->getComposer()->getInstallationManager();
298
    $install_path = $manager->getInstaller($package->getType())->getInstallPath($package);
299
300
    // Set up a downloader.
301
    $downloader = new RemoteFilesystem($this->io, $this->composer->getConfig());
302
303
    // Track applied patches in the package info in installed.json
304
    $localRepository = $this->composer->getRepositoryManager()->getLocalRepository();
305
    $localPackage = $localRepository->findPackage($package_name, $package->getVersion());
306
    $extra = $localPackage->getExtra();
307
    $extra['patches_applied'] = array();
308
309
    foreach ($this->patches[$package_name] as $description => $url) {
310
      $this->io->write('    <info>' . $url . '</info> (<comment>' . $description. '</comment>)');
311
      try {
312
        $this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::PRE_PATCH_APPLY, $package, $url, $description));
313
        $this->getAndApplyPatch($downloader, $install_path, $url, $package);
314
        $this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::POST_PATCH_APPLY, $package, $url, $description));
315
        $extra['patches_applied'][$description] = $url;
316
      }
317
      catch (\Exception $e) {
318
        $this->io->write('   <error>Could not apply patch! Skipping. The error was: ' . $e->getMessage() . '</error>');
319
        if ($exitOnFailure) {
320
          throw new \Exception("Cannot apply patch $description ($url)!");
321
        }
322
      }
323
    }
324
    $localPackage->setExtra($extra);
325
326
    $this->io->write('');
327
    $this->writePatchReport($this->patches[$package_name], $install_path);
328
  }
329
330
  /**
331
   * Get a Package object from an OperationInterface object.
332
   *
333
   * @param OperationInterface $operation
334
   * @return PackageInterface
335
   * @throws \Exception
336
   */
337
  protected function getPackageFromOperation(OperationInterface $operation) {
338
    if ($operation instanceof InstallOperation) {
0 ignored issues
show
Bug introduced by
The class Composer\DependencyResol...ration\InstallOperation does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
339
      $package = $operation->getPackage();
340
    }
341
    elseif ($operation instanceof UpdateOperation) {
0 ignored issues
show
Bug introduced by
The class Composer\DependencyResol...eration\UpdateOperation does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
342
      $package = $operation->getTargetPackage();
343
    }
344
    else {
345
      throw new \Exception('Unknown operation: ' . get_class($operation));
346
    }
347
348
    return $package;
349
  }
350
351
  /**
352
   * Apply a patch on code in the specified directory.
353
   *
354
   * @param RemoteFilesystem $downloader
355
   * @param $install_path
356
   * @param $patch_url
357
   * @param PackageInterface $package
358
   * @throws \Exception
359
   */
360
  protected function getAndApplyPatch(RemoteFilesystem $downloader, $install_path, $patch_url, PackageInterface $package) {
361
362
    // Local patch file.
363
    if (file_exists($patch_url)) {
364
      $filename = realpath($patch_url);
365
    }
366
    else {
367
      // Generate random (but not cryptographically so) filename.
368
      $filename = uniqid(sys_get_temp_dir().'/') . ".patch";
369
370
      // Download file from remote filesystem to this location.
371
      $hostname = parse_url($patch_url, PHP_URL_HOST);
372
373
      try {
374
        $downloader->copy($hostname, $patch_url, $filename, false);
375
      } catch (\Exception $e) {
376
        // In case of an exception, retry once as the download might
377
        // have failed due to intermittent network issues.
378
        $downloader->copy($hostname, $patch_url, $filename, false);
379
      }
380
    }
381
382
    // The order here is intentional. p1 is most likely to apply with git apply.
383
    // p0 is next likely. p2 is extremely unlikely, but for some special cases,
384
    // it might be useful. p4 is useful for Magento 2 patches
385
    $patch_levels = array('-p1', '-p0', '-p2', '-p4');
386
387
    // Check for specified patch level for this package.
388
    $extra = $this->composer->getPackage()->getExtra();
389
    if (!empty($extra['patchLevel'][$package->getName()])){
390
      $patch_levels = array($extra['patchLevel'][$package->getName()]);
391
    }
392
    // Attempt to apply with git apply.
393
    $patched = $this->applyPatchWithGit($install_path, $patch_levels, $filename);
394
395
    // In some rare cases, git will fail to apply a patch, fallback to using
396
    // the 'patch' command.
397
    if (!$patched) {
398
      foreach ($patch_levels as $patch_level) {
399
        // --no-backup-if-mismatch here is a hack that fixes some
400
        // differences between how patch works on windows and unix.
401
        if ($patched = $this->executeCommand("patch %s --no-backup-if-mismatch -d %s < %s", $patch_level, $install_path, $filename)) {
402
          break;
403
        }
404
      }
405
    }
406
407
    // Clean up the temporary patch file.
408
    if (isset($hostname)) {
409
      unlink($filename);
410
    }
411
    // If the patch *still* isn't applied, then give up and throw an Exception.
412
    // Otherwise, let the user know it worked.
413
    if (!$patched) {
414
      throw new \Exception("Cannot apply patch $patch_url");
415
    }
416
  }
417
418
  /**
419
   * Checks if the root package enables patching.
420
   *
421
   * @return bool
422
   *   Whether patching is enabled. Defaults to TRUE.
423
   */
424
  protected function isPatchingEnabled() {
425
    $extra = $this->composer->getPackage()->getExtra();
426
427
    if (empty($extra['patches']) && empty($extra['patches-ignore']) && !isset($extra['patches-file'])) {
428
      // The root package has no patches of its own, so only allow patching if
429
      // it has specifically opted in.
430
      return isset($extra['enable-patching']) ? $extra['enable-patching'] : FALSE;
431
    }
432
    else {
433
      return TRUE;
434
    }
435
  }
436
437
  /**
438
   * Writes a patch report to the target directory.
439
   *
440
   * @param array $patches
441
   * @param string $directory
442
   */
443
  protected function writePatchReport($patches, $directory) {
444
    $output = "This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches)\n";
445
    $output .= "Patches applied to this directory:\n\n";
446
    foreach ($patches as $description => $url) {
447
      $output .= $description . "\n";
448
      $output .= 'Source: ' . $url . "\n\n\n";
449
    }
450
    file_put_contents($directory . "/PATCHES.txt", $output);
451
  }
452
453
  /**
454
   * Executes a shell command with escaping.
455
   *
456
   * @param string $cmd
457
   * @return bool
458
   */
459
  protected function executeCommand($cmd) {
460
    // Shell-escape all arguments except the command.
461
    $args = func_get_args();
462
    foreach ($args as $index => $arg) {
463
      if ($index !== 0) {
464
        $args[$index] = escapeshellarg($arg);
465
      }
466
    }
467
468
    // And replace the arguments.
469
    $command = call_user_func_array('sprintf', $args);
470
    $output = '';
471
    if ($this->io->isVerbose()) {
472
      $this->io->write('<comment>' . $command . '</comment>');
473
      $io = $this->io;
474
      $output = function ($type, $data) use ($io) {
475
        if ($type == Process::ERR) {
476
          $io->write('<error>' . $data . '</error>');
477
        }
478
        else {
479
          $io->write('<comment>' . $data . '</comment>');
480
        }
481
      };
482
    }
483
    return ($this->executor->execute($command, $output) == 0);
484
  }
485
486
  /**
487
   * Recursively merge arrays without changing data types of values.
488
   *
489
   * Does not change the data types of the values in the arrays. Matching keys'
490
   * values in the second array overwrite those in the first array, as is the
491
   * case with array_merge.
492
   *
493
   * @param array $array1
494
   *   The first array.
495
   * @param array $array2
496
   *   The second array.
497
   * @return array
498
   *   The merged array.
499
   *
500
   * @see http://php.net/manual/en/function.array-merge-recursive.php#92195
501
   */
502
  protected function arrayMergeRecursiveDistinct(array $array1, array $array2) {
503
    $merged = $array1;
504
505
    foreach ($array2 as $key => &$value) {
506
      if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
507
        $merged[$key] = $this->arrayMergeRecursiveDistinct($merged[$key], $value);
508
      }
509
      else {
510
        $merged[$key] = $value;
511
      }
512
    }
513
514
    return $merged;
515
  }
516
517
  /**
518
   * Attempts to apply a patch with git apply.
519
   *
520
   * @param $install_path
521
   * @param $patch_levels
522
   * @param $filename
523
   *
524
   * @return bool
525
   *   TRUE if patch was applied, FALSE otherwise.
526
   */
527
  protected function applyPatchWithGit($install_path, $patch_levels, $filename) {
528
    // Do not use git apply unless the install path is itself a git repo
529
    // @see https://stackoverflow.com/a/27283285
530
    if (!is_dir($install_path . '/.git')) {
531
      return FALSE;
532
    }
533
534
    $patched = FALSE;
535
    foreach ($patch_levels as $patch_level) {
536
      if ($this->io->isVerbose()) {
537
        $comment = 'Testing ability to patch with git apply.';
538
        $comment .= ' This command may produce errors that can be safely ignored.';
539
        $this->io->write('<comment>' . $comment . '</comment>');
540
      }
541
      $checked = $this->executeCommand('git -C %s apply --check -v %s %s', $install_path, $patch_level, $filename);
542
      $output = $this->executor->getErrorOutput();
543
      if (substr($output, 0, 7) == 'Skipped') {
544
        // Git will indicate success but silently skip patches in some scenarios.
545
        //
546
        // @see https://github.com/cweagans/composer-patches/pull/165
547
        $checked = FALSE;
548
      }
549
      if ($checked) {
550
        // Apply the first successful style.
551
        $patched = $this->executeCommand('git -C %s apply %s %s', $install_path, $patch_level, $filename);
552
        break;
553
      }
554
    }
555
    return $patched;
556
  }
557
558
}
559