• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eiriksm / cosy-composer / 14250315248

03 Apr 2025 06:37PM UTC coverage: 87.091% (+0.1%) from 86.96%
14250315248

push

github

web-flow
Make sure we handle some common errors with group PRs (#416)

15 of 15 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

1970 of 2262 relevant lines covered (87.09%)

44.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.16
/src/Updater/IndividualUpdater.php
1
<?php
2

3
namespace eiriksm\CosyComposer\Updater;
4

5
use Composer\Semver\Comparator;
6
use Composer\Semver\Semver;
7
use eiriksm\CosyComposer\CosyLogger;
8
use eiriksm\CosyComposer\GroupUpdateItem;
9
use eiriksm\CosyComposer\Helpers;
10
use eiriksm\CosyComposer\IndividualUpdateItem;
11
use eiriksm\CosyComposer\LockDataComparer;
12
use eiriksm\CosyComposer\Message;
13
use eiriksm\CosyComposer\ProcessFactoryWrapper;
14
use eiriksm\CosyComposer\PrParamsCreator;
15
use eiriksm\CosyComposer\UpdateItemInterface;
16
use eiriksm\ViolinistMessages\UpdateListItem;
17
use eiriksm\ViolinistMessages\ViolinistUpdate;
18
use Github\Exception\ValidationFailedException;
19
use Violinist\ComposerLockData\ComposerLockData;
20
use Violinist\ComposerUpdater\Exception\ComposerUpdateProcessFailedException;
21
use Violinist\ComposerUpdater\Exception\NotUpdatedException;
22
use Violinist\ComposerUpdater\Updater;
23
use Violinist\Config\Config;
24
use Violinist\ProjectData\ProjectData;
25

26
class IndividualUpdater extends BaseUpdater
27
{
28
    /**
29
     * @var string
30
     */
31
    private $composerJsonDir;
32

33
    public function __construct()
34
    {
35
    }
131✔
36

37
    public function setComposerJsonDir($dir)
38
    {
39
        $this->composerJsonDir = $dir;
123✔
40
    }
41

42
    /**
43
     * {@inheritdoc}
44
     */
45
    public function handleUpdate($data, $lockdata, $cdata, $one_pr_per_dependency, $initial_lock_file_data, $prs_named, $default_base, $hostname, $default_branch, $alerts, $is_allowed_out_of_date_pr, Config $config)
46
    {
47
        $this->initialComposerLockData = $initial_lock_file_data;
123✔
48
        $can_update_beyond = $config->shouldAllowUpdatesBeyondConstraint();
123✔
49
        $max_number_of_prs = $config->getNumberOfAllowedPrs();
123✔
50
        $data = $this->convertDataToDto($data);
123✔
51
        // And now convert the data to DTOs for the groups.
52
        $groups = self::createGroups($data, $config);
123✔
53
        // Now we have the groups, we can loop through them and handle them. In
54
        // the process we will also remove items from the individual items.
55
        foreach ($groups as $group) {
123✔
56
            // Alright, let's take the rule, and remove any items that are in
57
            // composer.json and also in the group.
58
            $has_match = false;
6✔
59
            foreach ($data as $key => $item) {
6✔
60
                // Now let's see if it matches the group, and remove it from the
61
                // updates.
62
                if (!$group->groupRuleMatches($item->getPackageName())) {
6✔
63
                    continue;
5✔
64
                }
65
                // Not sure how it can not have a match somewhere, but hey.
66
                $has_match = true;
6✔
67
                unset($data[$key]);
6✔
68
            }
69
            if ($has_match) {
6✔
70
                // Of course, also add it to the actual jobs we will be doing.
71
                $data[] = $group;
6✔
72
            }
73
        }
74
        foreach ($data as $item) {
123✔
75
            $item_name = $item->getPackageName();
123✔
76
            $security_update = false;
123✔
77
            $package_name_in_composer_json = $item_name;
123✔
78
            try {
79
                $package_name_in_composer_json = Helpers::getComposerJsonName($cdata, $item_name, $this->composerJsonDir);
123✔
80
            } catch (\Exception $e) {
16✔
81
            }
82
            if (isset($alerts[$package_name_in_composer_json])) {
123✔
83
                $security_update = true;
16✔
84
            }
85
            if ($max_number_of_prs && $this->getPrCount() >= $max_number_of_prs) {
123✔
86
                if ($security_update && $config->shouldAllowSecurityUpdatesOnConcurrentLimit()) {
5✔
87
                    $this->log(sprintf('The concurrent limit (%d) is reached, but the update of %s is a security update, so we will try to update it anyway.', $max_number_of_prs, $package_name_in_composer_json));
1✔
88
                } elseif (!in_array($item_name, $is_allowed_out_of_date_pr)) {
5✔
89
                    $this->log(
4✔
90
                        sprintf(
4✔
91
                            'Skipping %s because the number of max concurrent PRs (%d) seems to have been reached',
4✔
92
                            $item_name,
4✔
93
                            $max_number_of_prs
4✔
94
                        ),
4✔
95
                        Message::CONCURRENT_THROTTLED,
4✔
96
                        [
4✔
97
                            'package' => $item_name,
4✔
98
                        ]
4✔
99
                    );
4✔
100
                    continue;
4✔
101
                }
102
            }
103
            $this->handleUpdateItem(
122✔
104
                $item,
122✔
105
                $lockdata,
122✔
106
                $cdata,
122✔
107
                $one_pr_per_dependency,
122✔
108
                $initial_lock_file_data,
122✔
109
                $prs_named,
122✔
110
                $default_base,
122✔
111
                $hostname,
122✔
112
                $default_branch,
122✔
113
                $security_update,
122✔
114
                $config,
122✔
115
                $can_update_beyond
122✔
116
            );
122✔
117
        }
118
    }
119

120
    protected function handleGroup(GroupUpdateItem $item, $lockdata, $cdata, $one_pr_per_dependency, $lock_file_contents, $prs_named, $default_base, $hostname, $default_branch, bool $security_update, Config $global_config, $can_update_beyond)
121
    {
122
        // @todo: This should rather take the config from the rule.
123
        $config = $global_config;
6✔
124
        // Alright, its a group. Let's gather all the package names that are in
125
        // composer json, and also matches.
126
        $package_matches = [];
6✔
127
        $composer_json = json_decode(file_get_contents($this->composerJsonDir . '/composer.json'));
6✔
128
        if (!empty($composer_json->require)) {
6✔
129
            foreach ($composer_json->require as $key => $value) {
6✔
130
                if ($item->groupRuleMatches($key)) {
6✔
131
                    $package_matches[] = $key;
6✔
132
                }
133
            }
134
        }
135
        if (!empty($composer_json->{'require-dev'})) {
6✔
136
            foreach ($composer_json->{'require-dev'} as $key => $value) {
×
137
                if ($item->groupRuleMatches($key)) {
×
138
                    $package_matches[] = $key;
×
139
                }
140
            }
141
        }
142
        if (empty($package_matches)) {
6✔
143
            // That's very strange. Let's call it an error.
144
            throw new \RuntimeException('No packages found in composer.json that matches the group rule');
×
145
        }
146
        $branch_name = '';
6✔
147
        $pr_params = [];
6✔
148
        try {
149
            // Create a branch. This should be specified in the rule config, yeah?
150
            $rule = $item->getRule();
6✔
151
            if (empty($rule->name)) {
6✔
152
                throw new \RuntimeException('The group rule does not have a name');
×
153
            }
154
            // The branch will be based on either the name, which we now know
155
            // exists, or a slug.
156
            $branch_name = Helpers::createBranchNameForGroup($rule, $config);
6✔
157
            $this->switchBranch($branch_name);
6✔
158
            // Now let's update them.
159
            $array_copy = $package_matches;
6✔
160
            $package_name = array_shift($package_matches);
6✔
161
            $updater = $this->getUpdater($package_name);
6✔
162
            $updater->setPackagesToCheckHasUpdated($package_matches);
6✔
163
            array_shift($array_copy);
6✔
164
            if (!empty($array_copy)) {
6✔
165
                $updater->setBundledPackages($array_copy);
6✔
166
            }
167
            $updater->setWithUpdate($config->shouldUpdateWithDependencies());
6✔
168
            $updater->setRunScripts($config->shouldRunScripts());
6✔
169
            if (!$lock_file_contents) {
6✔
170
                throw new \Exception('The group update can not be run with composer require');
×
171
            } else {
172
                $this->log('Running composer update for package ' . $package_name);
6✔
173
                $updater->executeUpdate();
6✔
174
            }
175
            // I guess at this point we know that something updated. Which is good. Let's create a PR then.
176
            $pr_params_creator = $this->getPrParamsCreator();
3✔
177
            $new_lock_data = json_decode(file_get_contents($this->composerJsonDir . '/composer.lock'));
3✔
178
            $post_update_lock = ComposerLockData::createFromString(json_encode($new_lock_data));
3✔
179
            $comparer = new LockDataComparer($lockdata, $new_lock_data);
3✔
180
            $package_lock_data = ComposerLockData::createFromString(json_encode($lockdata));
3✔
181
            $update_list = $comparer->getUpdateList();
3✔
182
            $update_array = array_map(function (UpdateListItem $update) use ($lockdata, $package_lock_data, $post_update_lock) {
3✔
183
                $update_obj = new ViolinistUpdate();
3✔
184
                $update_obj->setName($update->getPackageName());
3✔
185
                $update_obj->setCurrentVersion($update->getOldVersion());
3✔
186
                $update_obj->setNewVersion($update->getNewVersion());
3✔
187
                $package_name = $update->getPackageName();
3✔
188
                $pre_update_data = $package_lock_data->getPackageData($package_name);
3✔
189
                $post_update_data = $post_update_lock->getPackageData($package_name);
3✔
190
                $version_from = $update->getOldVersion();
3✔
191
                $version_to = $update->getNewVersion();
3✔
192
                $this->log('Trying to retrieve changelog for ' . $package_name);
3✔
193
                $changelog = null;
3✔
194
                $changed_files = [];
3✔
195
                try {
196
                    $changelog = $this->retrieveChangeLog($package_name, $lockdata, $version_from, $version_to);
3✔
197
                    $update_obj->setChangelog($changelog->getAsMarkdown());
1✔
198
                    $this->log('Changelog retrieved');
1✔
199
                } catch (\Throwable $e) {
3✔
200
                    // If the changelog can not be retrieved, we can live with that.
201
                    $this->log('Exception for changelog: ' . $e->getMessage());
3✔
202
                }
203
                try {
204
                    $changed_files = $this->retrieveChangedFiles($package_name, $lockdata, $version_from, $version_to);
3✔
205
                    $update_obj->setChangedFiles($changed_files);
3✔
206
                    $this->log('Changed files retrieved');
3✔
207
                } catch (\Throwable $e) {
×
208
                    // If the changed files can not be retrieved, we can live with that.
209
                    $this->log('Exception for retrieving changed files: ' . $e->getMessage());
×
210
                }
211
                // Let's try to find all of the tags between those commit shas.
212
                $release_links = null;
3✔
213
                try {
214
                    $release_links = $this->getReleaseLinks($lockdata, $package_name, $pre_update_data, $post_update_data);
3✔
215
                    $update_obj->setPackageReleaseNotes($release_links);
3✔
216
                } catch (\Throwable $e) {
×
217
                    $this->log('Retrieving links to releases failed');
×
218
                }
219
                return $update_obj;
3✔
220
            }, $update_list);
3✔
221
            $body = $pr_params_creator->createBodyForGroup($rule->name, $update_array);
3✔
222
            $title = sprintf('Update group `%s`', $rule->name);
3✔
223
            $pr_params = $pr_params_creator->getPrParamsForGroup($this->forkUser, $this->isPrivate, $this->slug, $branch_name, $body, $title, $default_branch, $config);
3✔
224
            $this->commitFilesForGroup($rule->name, $config);
3✔
225
            $this->runAuthExport($hostname);
3✔
226
            $this->pushCode($branch_name, $default_base, $lock_file_contents);
3✔
227
            $pullRequest = $this->createPullrequest($pr_params);
3✔
228
        } catch (NotUpdatedException $e) {
5✔
229
            // Not updated because of the composer command, not the
230
            // restriction itself.
231
            $item_data_items = $item->getData();
3✔
232
            foreach ($item_data_items as $update_item) {
3✔
233
                $why_not_name = $update_item->name;
3✔
234
                $why_not_version = trim($update_item->latest);
3✔
235
                $not_updated_context = [
3✔
236
                    'package' => $why_not_name,
3✔
237
                ];
3✔
238
                $command = [
3✔
239
                    'composer',
3✔
240
                    'why-not',
3✔
241
                    $why_not_name,
3✔
242
                    $why_not_version,
3✔
243
                ];
3✔
244
                $this->execCommand($command, false);
3✔
245
                $this->log($this->getLastStdErr(), Message::COMMAND, [
3✔
246
                    'command' => implode(' ', $command),
3✔
247
                    'package' => $why_not_name,
3✔
248
                    'type' => 'stderr',
3✔
249
                ]);
3✔
250
                $this->log($this->getLastStdOut(), Message::COMMAND, [
3✔
251
                    'command' => implode(' ', $command),
3✔
252
                    'package' => $why_not_name,
3✔
253
                    'type' => 'stdout',
3✔
254
                ]);
3✔
255
                $this->log(sprintf('Package %s was not updated', $update_item->name), Message::NOT_UPDATED, $not_updated_context);
3✔
256
            }
257
        } catch (ValidationFailedException $e) {
3✔
258
            // @todo: Do some better checking. Could be several things, this.
259
            $this->handlePossibleUpdatePrScenario($e, $branch_name, $pr_params, $prs_named, $config, $security_update);
1✔
260
            // If it failed validation because it already exists, we also want to make sure all outdated PRs are
261
            // closed.
262
            $raw_item = $item->getData();
1✔
263
            if (!empty($prs_named[$branch_name]['number'])) {
1✔
264
                // @todo: Count the PR and close outdated.
265
            }
266
        } catch (\Gitlab\Exception\RuntimeException $e) {
2✔
267
            $this->handlePossibleUpdatePrScenario($e, $branch_name, $pr_params, $prs_named, $config, $security_update);
1✔
268
            if (!empty($prs_named[$branch_name]['number'])) {
1✔
269
                // @todo: Count the PR and close outdated.
270
            }
271
        } catch (ComposerUpdateProcessFailedException $e) {
1✔
272
            $this->log('Caught an exception: ' . $e->getMessage(), 'error');
1✔
273
            $this->log($e->getErrorOutput(), Message::COMMAND, [
1✔
274
                'type' => 'exit_code_output',
1✔
275
                'package' => $package_name,
1✔
276
            ]);
1✔
UNCOV
277
        } catch (\Throwable $e) {
×
278
            // @todo: Should probably handle this in some way.
279
            $this->log('Caught an exception: ' . $e->getMessage(), 'error');
×
280
        }
281
        $this->executePostUpdateStep($default_branch, $lock_file_contents, $config);
6✔
282
    }
283

284
    protected function handleUpdateItem(UpdateItemInterface $item_object, $lockdata, $cdata, $one_pr_per_dependency, $lock_file_contents, $prs_named, $default_base, $hostname, $default_branch, bool $security_update, Config $global_config, $can_update_beyond)
285
    {
286
        if ($item_object instanceof GroupUpdateItem) {
122✔
287
            return $this->handleGroup($item_object, $lockdata, $cdata, $one_pr_per_dependency, $lock_file_contents, $prs_named, $default_base, $hostname, $default_branch, $security_update, $global_config, $can_update_beyond);
6✔
288
        }
289
        if (!$item_object instanceof IndividualUpdateItem) {
116✔
290
            throw new \RuntimeException('The item object is not an instance of IndividualUpdateItem');
×
291
        }
292
        $item = $item_object->getRawData();
116✔
293
        // Default to global config.
294
        $config = $global_config;
116✔
295
        $should_indicate_can_not_update_if_unupdated = false;
116✔
296
        $package_name = $item->name;
116✔
297
        $branch_name = '';
116✔
298
        $pr_params = [];
116✔
299
        try {
300
            $package_lock_data = ComposerLockData::createFromString(json_encode($lockdata));
116✔
301
            $pre_update_data = $package_lock_data->getPackageData($package_name);
109✔
302
            $version_from = $item->version;
108✔
303
            $version_to = $item->latest;
108✔
304
            // See where this package is.
305
            try {
306
                $package_name_in_composer_json = Helpers::getComposerJsonName($cdata, $package_name, $this->composerJsonDir);
108✔
307
                $config = $global_config->getConfigForPackage($package_name_in_composer_json);
107✔
308
            } catch (\Exception $e) {
3✔
309
                // If this was a package that we somehow got because we have allowed to update other than direct
310
                // dependencies we can avoid re-throwing this.
311
                $config = $global_config->getConfigForPackage($package_name);
3✔
312
                if ($global_config->shouldCheckDirectOnly()) {
3✔
313
                    throw $e;
1✔
314
                }
315
                // Taking a risk :o.
316
                $package_name_in_composer_json = $package_name;
2✔
317
            }
318
            $req_item = '';
107✔
319
            $is_require_dev = false;
107✔
320
            if (!empty($cdata->{'require-dev'}->{$package_name_in_composer_json})) {
107✔
321
                $req_item = $cdata->{'require-dev'}->{$package_name_in_composer_json};
6✔
322
                $is_require_dev = true;
6✔
323
            } else {
324
                // @todo: Support getting req item from a merge plugin as well.
325
                if (isset($cdata->{'require'}->{$package_name_in_composer_json})) {
102✔
326
                    $req_item = $cdata->{'require'}->{$package_name_in_composer_json};
102✔
327
                }
328
            }
329
            $should_update_beyond = false;
107✔
330
            // See if the new version seems to satisfy the constraint. Unless the constraint is dev related somehow.
331
            try {
332
                if (strpos((string) $req_item, 'dev') === false && !Semver::satisfies($version_to, (string) $req_item)) {
107✔
333
                    // Well, unless we have actually disallowed this through config.
334
                    $should_update_beyond = true;
25✔
335
                    if (!$can_update_beyond) {
25✔
336
                        // Let's instead try to update within the constraint.
337
                        $should_update_beyond = false;
2✔
338
                        $should_indicate_can_not_update_if_unupdated = true;
102✔
339
                    }
340
                }
341
            } catch (\Exception $e) {
7✔
342
                // Could be, some times, that we try to check a constraint that semver does not recognize. That is
343
                // totally fine.
344
            }
345

346
            // Create a new branch.
347
            $branch_name = Helpers::createBranchName($item, $one_pr_per_dependency, $config);
107✔
348
            $this->switchBranch($branch_name);
107✔
349
            // Try to use the same version constraint.
350
            $version = (string) $req_item;
107✔
351
            // @todo: This is not nearly something that covers the world of constraints. Probably possible to use
352
            // something from composer itself here.
353
            $constraint = '';
107✔
354
            if (!empty($version[0])) {
107✔
355
                switch ($version[0]) {
107✔
356
                    case '^':
107✔
357
                        $constraint = '^';
70✔
358
                        break;
70✔
359

360
                    case '~':
39✔
361
                        $constraint = '~';
12✔
362
                        break;
12✔
363

364
                    default:
365
                        $constraint = '';
27✔
366
                        break;
27✔
367
                }
368
            }
369
            $update_with_deps = $config->shouldUpdateWithDependencies();
107✔
370
            $updater = $this->getUpdater($package_name);
107✔
371
            // See if this package has any bundled updates.
372
            $bundled_packages = $config->getBundledPackagesForPackage($package_name);
107✔
373
            if (!empty($bundled_packages)) {
107✔
374
                $updater->setBundledPackages($bundled_packages);
1✔
375
            }
376
            $updater->setWithUpdate($update_with_deps);
107✔
377
            $updater->setConstraint($constraint);
107✔
378
            $updater->setDevPackage($is_require_dev);
107✔
379
            $updater->setRunScripts($config->shouldRunScripts());
107✔
380
            if ($config->shouldUpdateIndirectWithDirect()) {
107✔
381
                $updater->setShouldThrowOnUnupdated(false);
6✔
382
                if (!empty($item->child_with_update)) {
6✔
383
                    $updater->setShouldThrowOnUnupdated(true);
6✔
384
                    $updater->setPackagesToCheckHasUpdated([$item->child_with_update]);
6✔
385
                }
386
                // But really, we should now have an array, shouldn't we?
387
                if (!empty($item->children_with_update) && is_array($item->children_with_update)) {
6✔
388
                    $updater->setShouldThrowOnUnupdated(true);
6✔
389
                    $updater->setPackagesToCheckHasUpdated($item->children_with_update);
6✔
390
                }
391
            }
392
            if (!$lock_file_contents || ($should_update_beyond && $can_update_beyond)) {
107✔
393
                $updater->executeRequire($version_to);
23✔
394
            } else {
395
                if (!empty($item->child_with_update)) {
85✔
396
                    $this->log(sprintf('Running composer update for package %s to update the indirect dependency %s', $package_name, $item->child_with_update));
6✔
397
                } else {
398
                    $this->log('Running composer update for package ' . $package_name);
79✔
399
                }
400
                $updater->executeUpdate();
85✔
401
            }
402
            $post_update_data = $updater->getPostUpdateData();
95✔
403
            if (isset($post_update_data->source) && $post_update_data->source->type == 'git' && isset($pre_update_data->source)) {
95✔
404
                $version_from = $pre_update_data->source->reference;
79✔
405
                $version_to = $post_update_data->source->reference;
79✔
406
            }
407
            // Now, see if the update was actually to the version we are expecting.
408
            // If we are updating to another dev version, composer show will tell us something like:
409
            // dev-master 15eb463
410
            // while the post update data version will still say:
411
            // dev-master.
412
            // So to compare these, we compare the hashes, if the version latest we are updating to
413
            // matches the dev regex.
414
            if (preg_match('/dev-\S* /', $item->latest)) {
95✔
415
                $sha = preg_replace('/dev-\S* /', '', $item->latest);
1✔
416
                // Now if the version_to matches this, we have updated to the expected version.
417
                if (strpos($version_to, $sha) === 0) {
1✔
418
                    $post_update_data->version = $item->latest;
1✔
419
                }
420
            }
421
            // If the item->latest key is set to dependencies, we actually want to allow the branch to change, since
422
            // the version of the package will of course be an actual version instead of the version called
423
            // "latest".
424
            if ('dependencies' !== $item->latest && $post_update_data->version != $item->latest) {
95✔
425
                $new_item = (object) [
12✔
426
                    'name' => $item->name,
12✔
427
                    'version' => $item->version,
12✔
428
                    'latest' => $post_update_data->version,
12✔
429
                ];
12✔
430
                $new_branch_name = Helpers::createBranchName($new_item, $config->shouldUseOnePullRequestPerPackage(), $config);
12✔
431
                $is_an_actual_upgrade = Comparator::greaterThan($post_update_data->version, $item->version);
12✔
432
                $old_item_is_branch = strpos($item->version, 'dev-') === 0;
12✔
433
                $new_item_is_branch = strpos($post_update_data->version, 'dev-') === 0;
12✔
434
                if (!$old_item_is_branch && !$new_item_is_branch && !$is_an_actual_upgrade) {
12✔
435
                    throw new NotUpdatedException('The new version is lower than the installed version');
×
436
                }
437
                if ($branch_name !== $new_branch_name) {
12✔
438
                    $this->log(sprintf('Changing branch because of an unexpected update result. We expected the branch name to be %s but instead we are now switching to %s.', $branch_name, $new_branch_name));
9✔
439
                    $this->switchBranch($new_branch_name, false);
9✔
440
                    $branch_name = $new_branch_name;
9✔
441
                }
442
            }
443
            $this->log('Successfully ran command composer update for package ' . $package_name);
95✔
444
            $new_lock_data = json_decode(file_get_contents($this->composerJsonDir . '/composer.lock'));
95✔
445
            $list_item = new UpdateListItem($package_name, $post_update_data->version, $item->version);
95✔
446
            $this->log('Trying to retrieve changelog for ' . $package_name);
95✔
447
            $changelog = null;
95✔
448
            $changed_files = [];
95✔
449
            try {
450
                $changelog = $this->retrieveChangeLog($package_name, $lockdata, $version_from, $version_to);
95✔
451
                $this->log('Changelog retrieved');
×
452
            } catch (\Throwable $e) {
95✔
453
                // If the changelog can not be retrieved, we can live with that.
454
                $this->log('Exception for changelog: ' . $e->getMessage());
95✔
455
            }
456
            try {
457
                $changed_files = $this->retrieveChangedFiles($package_name, $lockdata, $version_from, $version_to);
95✔
458
                $this->log('Changed files retrieved');
79✔
459
            } catch (\Throwable $e) {
16✔
460
                // If the changed files can not be retrieved, we can live with that.
461
                $this->log('Exception for retrieving changed files: ' . $e->getMessage());
16✔
462
            }
463
            // Let's try to find all of the tags between those commit shas.
464
            $release_links = null;
95✔
465
            try {
466
                $release_links = $this->getReleaseLinks($lockdata, $package_name, $pre_update_data, $post_update_data);
95✔
467
            } catch (\Throwable $e) {
17✔
468
                $this->log('Retrieving links to releases failed');
17✔
469
            }
470
            $comparer = new LockDataComparer($lockdata, $new_lock_data);
95✔
471
            $update_list = $comparer->getUpdateList();
95✔
472
            $pr_params_creator = $this->getPrParamsCreator();
95✔
473
            $body = $pr_params_creator->createBody($item, $post_update_data, $changelog, $security_update, $update_list, $changed_files, $release_links);
95✔
474
            $title = $pr_params_creator->createTitle($item, $post_update_data, $security_update);
95✔
475
            if ($config->getDefaultBranch($security_update)) {
95✔
476
                $this->log('Default target branch branch from config is set to ' . $config->getDefaultBranch($security_update));
3✔
477
                $default_branch = $config->getDefaultBranch($security_update);
3✔
478
            }
479
            $pr_params = $pr_params_creator->getPrParams($this->forkUser, $this->isPrivate, $this->getSlug(), $branch_name, $body, $title, $default_branch, $config);
95✔
480
            // Check if this new branch name has a pr up-to-date.
481
            if (!Helpers::shouldUpdatePr($branch_name, $pr_params, $prs_named) && array_key_exists($branch_name, $prs_named)) {
95✔
482
                if (!$default_base) {
7✔
483
                    $this->log(sprintf('Skipping %s because a pull request already exists', $item->name), Message::PR_EXISTS, [
1✔
484
                        'package' => $item->name,
1✔
485
                    ]);
1✔
486
                    $this->countPR($item->name);
1✔
487
                    $this->closeOutdatedPrsForPackage($item->name, $item->version, $config, $prs_named[$branch_name]['number'], $prs_named, $default_branch);
1✔
488
                    return;
1✔
489
                }
490
                // Is the pr up to date?
491
                if ($prs_named[$branch_name]['base']['sha'] == $default_base) {
6✔
492
                    $this->log(sprintf('Skipping %s because a pull request already exists', $item->name), Message::PR_EXISTS, [
2✔
493
                        'package' => $item->name,
2✔
494
                    ]);
2✔
495
                    $this->countPR($item->name);
2✔
496
                    $pr_id = $prs_named[$branch_name]['number'];
2✔
497
                    $this->closeOutdatedPrsForPackage($item->name, $item->version, $config, $pr_id, $prs_named, $default_branch);
2✔
498
                    return;
2✔
499
                }
500
            }
501
            $this->commitFilesForPackage($list_item, $config, $is_require_dev);
92✔
502
            $this->runAuthExport($hostname);
91✔
503
            $this->pushCode($branch_name, $default_base, $lock_file_contents);
91✔
504
            $pullRequest = $this->createPullrequest($pr_params);
90✔
505
            if (!empty($pullRequest['html_url'])) {
75✔
506
                $this->log($pullRequest['html_url'], Message::PR_URL, [
74✔
507
                    'package' => $package_name,
74✔
508
                ]);
74✔
509

510
                Helpers::handleAutoMerge($this->client, $this->logger, $this->slug, $config, $pullRequest, $security_update);
74✔
511
                $this->handleLabels($config, $pullRequest, $security_update);
74✔
512
                if (!empty($pullRequest['number'])) {
74✔
513
                    $this->closeOutdatedPrsForPackage($item->name, $item->version, $config, $pullRequest['number'], $prs_named, $default_branch);
73✔
514
                }
515
            }
516
            $this->countPR($item->name);
75✔
517
        } catch (NotUpdatedException $e) {
41✔
518
            // Not updated because of the composer command, not the
519
            // restriction itself.
520
            if ($should_indicate_can_not_update_if_unupdated && isset($package_name) && isset($req_item) && isset($version_to)) {
14✔
521
                $message = sprintf('Package %s with the constraint %s can not be updated to %s.', $package_name, $req_item, $version_to);
1✔
522
                $this->log($message, Message::UNUPDATEABLE, [
1✔
523
                    'package' => $package_name,
1✔
524
                ]);
1✔
525
            } else {
526
                $why_not_name = $original_name = $item->name;
13✔
527
                $why_not_version = trim($item->latest);
13✔
528
                $not_updated_context = [
13✔
529
                    'package' => $why_not_name,
13✔
530
                ];
13✔
531
                if (!empty($item->child_latest) && !empty($item->child_with_update)) {
13✔
532
                    $why_not_name = $item->child_with_update;
2✔
533
                    $why_not_version = trim($item->child_latest);
2✔
534
                    $not_updated_context['package'] = $why_not_name;
2✔
535
                    $not_updated_context['parent_package'] = $original_name;
2✔
536
                }
537
                $command = [
13✔
538
                    'composer',
13✔
539
                    'why-not',
13✔
540
                    $why_not_name,
13✔
541
                    $why_not_version,
13✔
542
                ];
13✔
543
                $this->execCommand($command, false);
13✔
544
                $this->log($this->getLastStdErr(), Message::COMMAND, [
13✔
545
                    'command' => implode(' ', $command),
13✔
546
                    'package' => $why_not_name,
13✔
547
                    'type' => 'stderr',
13✔
548
                ]);
13✔
549
                $this->log($this->getLastStdOut(), Message::COMMAND, [
13✔
550
                    'command' => implode(' ', $command),
13✔
551
                    'package' => $why_not_name,
13✔
552
                    'type' => 'stdout',
13✔
553
                ]);
13✔
554
                if (!empty($item->child_with_update)) {
13✔
555
                    $this->log(sprintf("%s was not updated running composer update for direct dependency %s", $item->child_with_update, $package_name), Message::NOT_UPDATED, $not_updated_context);
2✔
556
                } else {
557
                    $this->log("$package_name was not updated running composer update", Message::NOT_UPDATED, $not_updated_context);
14✔
558
                }
559
            }
560
        } catch (ValidationFailedException $e) {
27✔
561
            // @todo: Do some better checking. Could be several things, this.
562
            $this->handlePossibleUpdatePrScenario($e, $branch_name, $pr_params, $prs_named, $config, $security_update);
10✔
563
            // If it failed validation because it already exists, we also want to make sure all outdated PRs are
564
            // closed.
565
            if (!empty($prs_named[$branch_name]['number'])) {
10✔
566
                $this->countPR($item->name);
10✔
567
                $this->closeOutdatedPrsForPackage($item->name, $item->version, $config, $prs_named[$branch_name]['number'], $prs_named, $default_branch);
10✔
568
            }
569
        } catch (\Gitlab\Exception\RuntimeException $e) {
17✔
570
            $this->handlePossibleUpdatePrScenario($e, $branch_name, $pr_params, $prs_named, $config, $security_update);
5✔
571
            if (!empty($prs_named[$branch_name]['number'])) {
5✔
572
                $this->countPR($item->name);
5✔
573
                $this->closeOutdatedPrsForPackage($item->name, $item->version, $config, $prs_named[$branch_name]['number'], $prs_named, $default_branch);
5✔
574
            }
575
        } catch (ComposerUpdateProcessFailedException $e) {
12✔
576
            $this->log('Caught an exception: ' . $e->getMessage(), 'error');
1✔
577
            $this->log($e->getErrorOutput(), Message::COMMAND, [
1✔
578
                'type' => 'exit_code_output',
1✔
579
                'package' => $package_name,
1✔
580
            ]);
1✔
581
        } catch (\Throwable $e) {
11✔
582
            // @todo: Should probably handle this in some way.
583
            $this->log('Caught an exception: ' . $e->getMessage(), 'error', [
11✔
584
                'package' => $package_name,
11✔
585
            ]);
11✔
586
        }
587
        $this->executePostUpdateStep($default_branch, $lock_file_contents, $config);
113✔
588
    }
589

590
    protected function executePostUpdateStep($default_branch, $lock_file_contents, Config $config)
591
    {
592
        $this->log('Checking out default branch - ' . $default_branch);
119✔
593
        $checkout_default_exit_code = $this->execCommand(['git', 'checkout', $default_branch], false);
119✔
594
        if ($checkout_default_exit_code) {
119✔
595
            $this->log($this->getLastStdErr());
×
596
            throw new \Exception('There was an error trying to check out the default branch. The process ended with exit code ' . $checkout_default_exit_code);
×
597
        }
598
        // Also do a git checkout of the files, since we want them in the state they were on the default branch
599
        $this->execCommand(['git', 'checkout', '.'], false);
119✔
600
        // Re-do composer install to make output better, and to make the lock file actually be there for
601
        // consecutive updates, if it is a project without it.
602
        if (!$lock_file_contents) {
119✔
603
            $this->execCommand(['rm', 'composer.lock']);
8✔
604
        }
605
        try {
606
            $this->doComposerInstall($config);
119✔
607
        } catch (\Throwable $e) {
1✔
608
            $this->log('Rolling back state on the default branch was not successful. Subsequent updates may be affected');
1✔
609
        }
610
    }
611

612
    protected function handleLabels(Config $config, $pullRequest, $security_update = false) : void
613
    {
614
        $labels_allowed = false;
91✔
615
        $labels_allowed_roles = [
91✔
616
            'agency',
91✔
617
            'enterprise',
91✔
618
        ];
91✔
619
        if ($this->projectData instanceof ProjectData && $this->projectData->getRoles()) {
91✔
620
            foreach ($this->projectData->getRoles() as $role) {
9✔
621
                if (in_array($role, $labels_allowed_roles)) {
9✔
622
                    $labels_allowed = true;
9✔
623
                }
624
            }
625
        }
626
        if (!$labels_allowed) {
91✔
627
            return;
82✔
628
        }
629
        Helpers::handleLabels($this->getPrClient(), $this->getLogger(), $this->slug, $config, $pullRequest, $security_update);
9✔
630
    }
631

632
    protected function handlePossibleUpdatePrScenario(\Exception $e, $branch_name, $pr_params, $prs_named, Config $config, $security_update = false)
633
    {
634
        $this->log('Had a problem with creating the pull request: ' . $e->getMessage(), 'error');
17✔
635
        if (Helpers::shouldUpdatePr($branch_name, $pr_params, $prs_named)) {
17✔
636
            $this->log('Will try to update the PR based on settings.');
15✔
637
            $this->getPrClient()->updatePullRequest($this->slug, $prs_named[$branch_name]['number'], $pr_params);
15✔
638
        }
639
        if (!empty($prs_named[$branch_name])) {
17✔
640
            Helpers::handleAutoMerge($this->client, $this->logger, $this->slug, $config, $prs_named[$branch_name], $security_update);
17✔
641
            $this->handleLabels($config, $prs_named[$branch_name], $security_update);
17✔
642
        }
643
    }
644

645
    /**
646
     * @return UpdateItemInterface[]
647
     */
648
    protected function convertDataToDto(array $data) : array
649
    {
650
        $new_items = [];
123✔
651
        foreach ($data as $item) {
123✔
652
            $new_items[] = new IndividualUpdateItem($item);
123✔
653
        }
654
        return $new_items;
123✔
655
    }
656

657
    /**
658
     * @param UpdateItemInterface[] $items
659
     * @return GroupUpdateItem[]
660
     */
661
    public static function createGroups(array $items, Config $config) : array
662
    {
663
        /** @var GroupUpdateItem[] $groups */
664
        $groups = [];
126✔
665
        $rules = $config->getRules();
126✔
666
        foreach ($items as $item) {
126✔
667
            if (!$item instanceof IndividualUpdateItem) {
126✔
668
                continue;
3✔
669
            }
670
            foreach ($rules as $rule) {
126✔
671
                $matches = $config->getMatcherFactory()->hasMatches($rule, $item->getPackageName());
8✔
672
                if (!$matches) {
8✔
673
                    continue;
6✔
674
                }
675
                // Well alright then. Let's create a group of it.
676
                $group = new GroupUpdateItem($rule, $item->getRawData(), $config);
7✔
677
                // We don't need multiple groups of the same rule. So create a
678
                // hash of it and use it as the key.
679
                $hash = md5(json_encode($rule));
7✔
680
                if (empty($groups[$hash])) {
7✔
681
                    $groups[$hash] = $group;
7✔
682
                    continue;
7✔
683
                }
684
                $groups[$hash]->addData($item->getRawData());
7✔
685
            }
686
        }
687
        return $groups;
126✔
688
    }
689

690
    protected function getPrParamsCreator() : PrParamsCreator
691
    {
692
        $pr_params_creator = new PrParamsCreator($this->messageFactory, $this->projectData);
98✔
693
        $pr_params_creator->setAssigneesAllowed($this->assigneesAllowed);
98✔
694
        $pr_params_creator->setLogger($this->getLogger());
98✔
695
        return $pr_params_creator;
98✔
696
    }
697

698
    protected function getUpdater(string $package_name) : Updater
699
    {
700
        $updater = new Updater($this->getCwd(), $package_name);
113✔
701
        $cosy_logger = new CosyLogger();
113✔
702
        $cosy_factory_wrapper = new ProcessFactoryWrapper();
113✔
703
        $cosy_factory_wrapper->setExecutor($this->executer);
113✔
704
        $cosy_logger->setLogger($this->getLogger());
113✔
705
        $updater->setLogger($cosy_logger);
113✔
706
        $updater->setProcessFactory($cosy_factory_wrapper);
113✔
707
        return $updater;
113✔
708
    }
709
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc