Completed
Push — master ( 8bf3f7...57b1db )
by Mike
02:52
created

UpgradeChanges::getChangedFiles()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 52
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 31
nc 8
nop 2
dl 0
loc 52
rs 7.2396
c 0
b 0
f 0

How to fix   Long Method   

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 Sugarcrm\UpgradeSpec\Element\Section;
4
5
use Sugarcrm\UpgradeSpec\Data\DataAwareInterface;
6
use Sugarcrm\UpgradeSpec\Data\DataAwareTrait;
7
use Sugarcrm\UpgradeSpec\Element\ElementInterface;
8
use Sugarcrm\UpgradeSpec\Spec\Context;
9
use Sugarcrm\UpgradeSpec\Template\RendererAwareInterface;
10
use Sugarcrm\UpgradeSpec\Template\RendererAwareTrait;
11
use Symfony\Component\Finder\Finder;
12
13
class UpgradeChanges implements ElementInterface, RendererAwareInterface, DataAwareInterface
14
{
15
    use RendererAwareTrait, DataAwareTrait;
16
17
    /**
18
     * @return string
19
     */
20
    public function getTitle()
21
    {
22
        return 'Review upgrade changes and fix possible customization conflicts';
23
    }
24
25
    /**
26
     * @return int
27
     */
28
    public function getOrder()
29
    {
30
        return 3;
31
    }
32
33
    /**
34
     * @param Context $context
35
     *
36
     * @return bool
37
     */
38
    public function isRelevantTo(Context $context)
39
    {
40
        return $context->getPackagesPath()
41
            && $this->getFlavPackages($context->getFlav(), $context->getPackagesPath());
42
    }
43
44
    /**
45
     * @param Context $context
46
     *
47
     * @return string
48
     */
49
    public function getBody(Context $context)
50
    {
51
        $packages = $this->getSuitablePackages($context);
52
53
        $modified = $deleted = [];
54
        if ($packages) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $packages of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
55
            list($modified, $deleted) = $this->getChangedFiles('/Users/m.kamornikov/Dev/sugarcrm/build/rome/builds/ult/sugarcrm', $packages);
56
            natsort($modified);
57
            natsort($deleted);
58
        }
59
60
        return $this->renderer->render('upgrade_changes', [
61
            'packages' => $packages,
62
            'upgrade_to' => $context->getUpgradeVersion(),
63
            'packages_path' => $context->getPackagesPath(),
64
            'modified_files' => $modified,
65
            'deleted_files' => $deleted,
66
        ]);
67
    }
68
69
    private function getChangedFiles($buildPath, $packages)
70
    {
71
        $changedFiles = $deletedFiles = $packageZips = [];
72
73
        foreach ($packages as $package) {
74
            $zip = new \ZipArchive();
75
            if (!$zip->open($package)) {
76
                throw new \RuntimeException(sprintf('Can\'t open zip archive: %s', $package));
77
            }
78
79
            $packageZips[$package] = $zip;
80
81
            eval(str_replace(['<?php', '<?', '?>'], '', $zip->getFromName(basename($package, '.zip') . DS . 'files.md5')));
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
82
            $packageChangedFiles = array_keys($md5_string);
0 ignored issues
show
Bug introduced by
The variable $md5_string does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
83
84
            if ($filesToRemove = $zip->getFromName('filesToRemove.txt')) {
85
                $packageDeletedFiles = explode(PHP_EOL, str_replace(["\r\n", "\r", "\n"], PHP_EOL, $filesToRemove));
86
            } else if ($filesToRemove = $zip->getFromName('filesToRemove.json')) {
87
                $packageDeletedFiles = json_decode($filesToRemove);
88
            } else {
89
                throw new \RuntimeException('Can\'t open filesToRemove');
90
            }
91
92
            $changedFiles = array_merge($changedFiles, array_combine($packageChangedFiles, array_fill(0, count($packageChangedFiles), $package)));
93
            $deletedFiles = array_diff(array_merge($deletedFiles, $packageDeletedFiles), $packageChangedFiles);
94
        }
95
96
        $changedFiles = array_keys(array_filter($changedFiles, function ($package, $changedFile) use ($buildPath, $packageZips) {
97
            if (($buildFile = @file_get_contents($buildPath . DS . $changedFile)) === false) {
98
                return false;
99
            }
100
            $packageFile = $packageZips[$package]->getFromName(basename($package, '.zip') . DS . $changedFile);
101
102
            return $this->getCheckSum($buildFile) != $this->getCheckSum($packageFile);
103
        }, ARRAY_FILTER_USE_BOTH));
104
105
        $deletedFiles = array_values(array_filter($deletedFiles));
106
107
        foreach ($packageZips as $zip) {
108
            $zip->close();
109
        }
110
111
        $changedFiles = array_values(array_filter($changedFiles, function ($file) use ($buildPath) {
112
            return file_exists($buildPath . '/custom/' . $file);
113
        }));
114
115
        $deletedFiles = array_values(array_filter($deletedFiles, function ($file) use ($buildPath) {
116
            return file_exists($buildPath . '/custom/' . $file);
117
        }));
118
119
        return [$changedFiles, $deletedFiles];
120
    }
121
122
    /**
123
     * @param $content
124
     *
125
     * @return string
126
     */
127
    private function getCheckSum($content)
128
    {
129
        // remove license comments
130
        $content = preg_replace('/\/\*.*?Copyright \(C\) SugarCRM.*?\*\//is', '', $content);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $content. This often makes code more readable.
Loading history...
131
132
        // remove blank lines
133
        $content = preg_replace('/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/', "\n", $content);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $content. This often makes code more readable.
Loading history...
134
135
        // change all line breaks to system line break
136
        $content = str_replace(["\r\n", "\r", "\n"], PHP_EOL, $content);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $content. This often makes code more readable.
Loading history...
137
138
        // remove trailing whitespaces
139
        $content = preg_replace('/^\s+|\s+$/m', '', $content);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $content. This often makes code more readable.
Loading history...
140
141
        return md5($content);
142
    }
143
144
    /**
145
     * @param Context $context
146
     *
147
     * @return array
148
     */
149
    private function getSuitablePackages(Context $context)
150
    {
151
        $chains = $this->getUpgradeChains(
152
            $this->getFlavPackages($context->getFlav(), $context->getPackagesPath()),
153
            $context->getBuildVersion(),
154
            $context->getUpgradeVersion()
155
        );
156
157
        if (!$chains) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chains of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
158
            return [];
159
        }
160
161
        usort($chains, function ($a1, $a2) {
162
            return count($a1) < count($a2) ? -1 : (count($a1) > count($a2) ? 1 : 0);
163
        });
164
165
        return $chains[0];
166
    }
167
168
    /**
169
     * Gets flav specific packages
170
     *
171
     * @param $flav
172
     * @param $packagesPath
173
     *
174
     * @return array
175
     */
176
    private function getFlavPackages($flav, $packagesPath)
177
    {
178
        $versionPattern = '/\d+\.\d+(\.\d+|\.x){1,2}/';
179
        $packagePattern = sprintf('/^Sugar%1$s-Upgrade-%2$s-to-%2$s.zip$/',
180
            ucfirst(mb_strtolower($flav)),
181
            trim($versionPattern, '/')
182
        );
183
184
        $packagesIterator = (new Finder())->files()->in($packagesPath)->name($packagePattern);
185
186
        $packages = [];
187
        foreach ($packagesIterator as $package) {
188
            if (preg_match_all($versionPattern, $package, $matches)) {
189
                $packages[$package->getFilename()] = [
190
                    'path' => $package->getRealPath(),
191
                    'from' => str_replace('.x', '', $matches[0][0]),
192
                    'to' => str_replace('.x', '', $matches[0][1])
193
                ];
194
            }
195
        }
196
197
        return $packages;
198
    }
199
200
    /**
201
     * Calculates an array of possible upgrade chains
202
     *
203
     * @param $packages
204
     * @param $buildVersion
205
     * @param $upgradeTo
206
     *
207
     * @return array
208
     */
209
    private function getUpgradeChains($packages, $buildVersion, $upgradeTo)
210
    {
211
        $versionMatrix = $this->getVersionMatrix($packages);
212
        $allVersions = array_keys($versionMatrix);
213
214
        $getExistingSubversions = function ($version) use ($allVersions) {
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $getExistingSubversions exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
215
            $existingVersions = [];
216
217
            $fromParts = explode('.', $version);
218
            foreach (range(2, count($fromParts)) as $length) {
219
                $subversion = implode('.', array_slice($fromParts, 0, $length));
220
                if (in_array($subversion, $allVersions)) {
221
                    $existingVersions[] = $subversion;
222
                }
223
            }
224
225
            return $existingVersions;
226
        };
227
228
        // init chains with starting versions
229
        $chains = array_map(function ($version) use ($buildVersion) {
230
            return [$version => $buildVersion];
231
        }, $getExistingSubversions($buildVersion));
232
233
        // finish early if starting / ending version doesn't exist
234
        if (!$chains || !in_array($upgradeTo, $allVersions)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chains of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
235
            return [];
236
        }
237
238
        // gets last key of assoc array
239
        $getLastKey = function ($array) {
240
            end($array);
241
242
            return key($array);
243
        };
244
245
        // find all chains
246
        while (true) {
247
            $fullChains = [];
248
            foreach ($chains as $index => $chain) {
249
                $fromVersion = $getLastKey($chain);
250
251
                // skip not interesting chains
252
                if (version_compare($fromVersion, $upgradeTo, '>=')) {
253
                    continue;
254
                }
255
256
                $validChain = false;
257
                foreach ($allVersions as $version) {
258
                    if (!empty($versionMatrix[$fromVersion][$version])) {
259
                        $to = $getExistingSubversions($version);
260
                        foreach ($to as $toVersion) {
261
262
                            if ($toVersion === $fromVersion
263
                                || version_compare($toVersion, $upgradeTo, '>')
264
                                || $chain[$getLastKey($chain)] === $version
265
                            ) {
266
                                continue;
267
                            }
268
269
                            $validChain = true;
270
                            $fullChains[] = array_merge($chain, [$toVersion => $version]);
271
                        }
272
                    }
273
                }
274
275
                // remove invalid chain
276
                if (!$validChain) {
277
                    unset($chains[$index]);
278
                }
279
            }
280
281
            if (!$fullChains) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fullChains of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
282
                break;
283
            }
284
285
            $chains = $fullChains;
286
        }
287
288
        $chains = array_map(function ($chain) use ($versionMatrix) {
289
            $keys = array_keys($chain);
290
            $values = array_values($chain);
291
292
            $packages = [];
293
            foreach (range(1, count($keys) - 1) as $index) {
294
                $packages[] = $versionMatrix[$keys[$index - 1]][$values[$index]];
295
            }
296
297
            return $packages;
298
        }, array_values($chains));
299
300
        return $chains;
301
    }
302
303
    /**
304
     * Creates version matrix
305
     *
306
     *       v1       v2       v3       v4
307
     *  v1    0     <path>   <path>      0
308
     *  v2    0        0     <path>      0
309
     *  v3    0        0        0     <path>
310
     *  v4    0        0        0        0
311
     *
312
     * @param $packages
313
     *
314
     * @return array
315
     */
316
    private function getVersionMatrix($packages)
317
    {
318
        $allVersions = array_unique(call_user_func_array('array_merge', array_map(function ($package) {
319
            return [$package['from'], $package['to']];
320
        }, $packages)));
321
322
        // sort versions (ASC)
323 View Code Duplication
        usort($allVersions, function ($v1, $v2) {
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...
324
            return version_compare($v1, $v2, '<') ? -1 : (version_compare($v1, $v2, '>') ? 1 : 0);
325
        });
326
327
        // create matrix and fill it with zeros
328
        $versionMatrix = call_user_func_array('array_merge',array_map(function ($version) use ($allVersions) {
329
            return [$version => array_combine($allVersions, array_fill(0, count($allVersions), 0))];
330
        }, $allVersions));
331
332
        // valid associations point to package path
333
        foreach ($packages as $name => $package) {
334
            $versionMatrix[$package['from']][$package['to']] = $package['path'];
335
        }
336
337
        return $versionMatrix;
338
    }
339
}
340