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

eiriksm / cosy-composer / 16469015360

23 Jul 2025 11:10AM UTC coverage: 86.578% (-0.4%) from 86.939%
16469015360

push

github

web-flow
More accurate parsing of named prs  (#427)

82 of 102 new or added lines in 9 files covered. (80.39%)

1 existing line in 1 file now uncovered.

2019 of 2332 relevant lines covered (86.58%)

44.84 hits per line

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

93.22
/src/Updater/BaseUpdater.php
1
<?php
2

3
namespace eiriksm\CosyComposer\Updater;
4

5
use eiriksm\CosyComposer\AssigneesAllowedTrait;
6
use eiriksm\CosyComposer\ComposerInstallTrait;
7
use eiriksm\CosyComposer\GitCommandsTrait;
8
use eiriksm\CosyComposer\Helpers;
9
use eiriksm\CosyComposer\Message;
10
use eiriksm\CosyComposer\PrCounterTrait;
11
use eiriksm\CosyComposer\ProcessFactoryWrapper;
12
use eiriksm\CosyComposer\ProviderInterface;
13
use eiriksm\CosyComposer\Providers\NamedPrs;
14
use eiriksm\CosyComposer\SlugAwareTrait;
15
use eiriksm\CosyComposer\TemporaryDirectoryAwareTrait;
16
use eiriksm\CosyComposer\TokenAwareTrait;
17
use eiriksm\CosyComposer\TokenChooser;
18
use eiriksm\ViolinistMessages\ViolinistMessages;
19
use Psr\Log\LoggerAwareTrait;
20
use Psr\Log\LoggerInterface;
21
use Violinist\ChangelogFetcher\ChangelogRetriever;
22
use Violinist\ChangelogFetcher\DependencyRepoRetriever;
23
use Violinist\ComposerLockData\ComposerLockData;
24
use Violinist\Config\Config;
25
use Violinist\GitLogFormat\ChangeLogData;
26
use Violinist\ProjectData\ProjectData;
27
use Wa72\SimpleLogger\ArrayLogger;
28
use function peterpostmann\uri\parse_uri;
29

30
abstract class BaseUpdater implements UpdaterInterface
31
{
32
    use LoggerAwareTrait;
33
    use ComposerInstallTrait;
34
    use PrCounterTrait;
35
    use GitCommandsTrait;
36
    use SlugAwareTrait;
37
    use TokenAwareTrait;
38
    use AssigneesAllowedTrait;
39
    use TemporaryDirectoryAwareTrait;
40

41
    /**
42
     * @var bool
43
     */
44
    protected $isPrivate = false;
45

46
    /**
47
     * @var \eiriksm\CosyComposer\CommandExecuter
48
     */
49
    protected $executer;
50

51
    /**
52
     * @var string
53
     */
54
    protected $cwd;
55

56
    /**
57
     * @var string
58
     */
59
    protected $initialComposerLockData;
60

61
    /**
62
     * @var \eiriksm\ViolinistMessages\ViolinistMessages
63
     */
64
    protected $messageFactory;
65

66
    /**
67
     * @var ProviderInterface
68
     */
69
    protected $client;
70

71
    /**
72
     * @var ProjectData
73
     */
74
    protected $projectData;
75

76
    /**
77
     * @var ChangelogRetriever
78
     */
79
    protected $fetcher;
80

81
    /**
82
     * @var string
83
     */
84
    protected $forkUser;
85

86
    public function setForkUser(string $user)
87
    {
88
        $this->forkUser = $user;
1✔
89
    }
90

91
    public function setIsPrivate($is_private)
92
    {
93
        $this->isPrivate = $is_private;
125✔
94
    }
95

96
    protected function preparePrClient()
97
    {
98
        // No-op, we just want it compatible with the interface.
99
    }
1✔
100

101
    protected function getPrClient() : ProviderInterface
102
    {
103
        return $this->client;
96✔
104
    }
105

106
    public function setClient(ProviderInterface $client)
107
    {
108
        $this->client = $client;
125✔
109
    }
110

111
    public function setProjectData(ProjectData $projectData)
112
    {
113
        $this->projectData = $projectData;
125✔
114
    }
115

116
    public function setMessageFactory(ViolinistMessages $messageFactory)
117
    {
118
        $this->messageFactory = $messageFactory;
125✔
119
    }
120

121
    protected function getlockFileContents()
122
    {
123
        return $this->initialComposerLockData;
96✔
124
    }
125

126
    protected function log($message, $type = 'message', $context = [])
127
    {
128
        $this->getLogger()->log('info', new Message($message, $type), $context);
125✔
129
    }
130

131
    /**
132
     * @return LoggerInterface
133
     */
134
    public function getLogger()
135
    {
136
        if (!$this->logger instanceof LoggerInterface) {
125✔
137
            $this->logger = new ArrayLogger();
×
138
        }
139
        return $this->logger;
125✔
140
    }
141

142
    /**
143
     * Executes a command.
144
     */
145
    protected function execCommand(array $command, $log = true, $timeout = 120, $env = [])
146
    {
147
        $this->executer->setCwd($this->getCwd());
123✔
148
        return $this->executer->executeCommand($command, $log, $timeout, $env);
123✔
149
    }
150

151
    public function setCWD($dir)
152
    {
153
        $this->cwd = $dir;
125✔
154
    }
155

156
    /**
157
     * @return string
158
     */
159
    public function getCwd()
160
    {
161
        return $this->cwd;
123✔
162
    }
163

164
    /**
165
     * @param \eiriksm\CosyComposer\CommandExecuter $executer
166
     */
167
    public function setExecuter($executer)
168
    {
169
        $this->executer = $executer;
131✔
170
    }
171

172
    /**
173
     * Helper to retrieve changelog.
174
     */
175
    public function retrieveChangeLog($package_name, $lockdata, $version_from, $version_to)
176
    {
177
        $lock_data_obj = new ComposerLockData();
107✔
178
        $lock_data_obj->setData($lockdata);
107✔
179
        $data = $lock_data_obj->getPackageData($package_name);
107✔
180
        if (empty($data->source->url)) {
106✔
181
            throw new \Exception('Unknown source or non-git source found for vendor/package. Aborting.');
17✔
182
        }
183
        $fetcher = $this->getFetcherForUrl($data->source->url);
89✔
184
        $log_obj = $fetcher->retrieveChangelog($package_name, $lockdata, $version_from, $version_to);
89✔
185
        $changelog_string = '';
6✔
186
        $json = json_decode($log_obj->getAsJson());
6✔
187
        foreach ($json as $item) {
6✔
188
            $changelog_string .= sprintf("%s %s\n", $item->hash, $item->message);
6✔
189
        }
190
        if (mb_strlen($changelog_string) > 60000) {
6✔
191
            // Truncate it to 60K.
192
            $changelog_string = mb_substr($changelog_string, 0, 60000);
1✔
193
            // Then split it into lines.
194
            $lines = explode("\n", $changelog_string);
1✔
195
            // Cut off the last one, since it could be partial.
196
            array_pop($lines);
1✔
197
            // Then append a line saying the changelog was too long.
198
            $lines[] = sprintf('%s ...more commits found, but message is too long for PR', $version_to);
1✔
199
            $changelog_string = implode("\n", $lines);
1✔
200
        }
201
        $log = ChangeLogData::createFromString($changelog_string);
6✔
202
        $git_url = preg_replace('/.git$/', '', $data->source->url);
6✔
203
        $repo_parsed = parse_uri($git_url);
6✔
204
        if (!empty($repo_parsed)) {
6✔
205
            switch ($repo_parsed['_protocol']) {
6✔
206
                case 'git@github.com':
6✔
207
                    $git_url = sprintf('https://github.com/%s', $repo_parsed['path']);
1✔
208
                    break;
1✔
209
            }
210
        }
211
        $log->setGitSource($git_url);
6✔
212
        return $log;
6✔
213
    }
214

215
    protected function retrieveChangedFiles($package_name, $lockdata, $version_from, $version_to)
216
    {
217
        $lock_data_obj = new ComposerLockData();
99✔
218
        $lock_data_obj->setData($lockdata);
99✔
219
        $data = $lock_data_obj->getPackageData($package_name);
99✔
220
        if (empty($data) || empty($data->source->url)) {
99✔
221
            throw new \Exception('Unknown source or non-git source found for vendor/package. Aborting.');
16✔
222
        }
223
        return $this->getFetcherForUrl($data->source->url)
83✔
224
            ->retrieveChangedFiles($package_name, $lockdata, $version_from, $version_to);
83✔
225
    }
226

227
    /**
228
     * @param $lockdata
229
     * @param $package_name
230
     * @param $pre_update_data
231
     * @param $post_update_data
232
     * @return array
233
     * @throws \Exception
234
     */
235
    public function getReleaseLinks($lockdata, $package_name, $pre_update_data, $post_update_data) : array
236
    {
237
        $extra_info = '';
99✔
238
        if (empty($pre_update_data->source->reference) || empty($post_update_data->source->reference)) {
99✔
239
            throw new \Exception('No SHAs to use to compare and retrieve tags for release links');
16✔
240
        }
241
        if (empty($post_update_data->source->url)) {
83✔
242
            throw new \Exception('No source URL to attempt to parse in post update data source');
×
243
        }
244
        $data = $this->getFetcherForUrl($post_update_data->source->url)->retrieveTagsBetweenShas($lockdata, $package_name, $pre_update_data->source->reference, $post_update_data->source->reference);
83✔
245
        $url = $post_update_data->source->url;
83✔
246
        $url = preg_replace('/.git$/', '', $url);
83✔
247
        $url_parsed = parse_url($url);
83✔
248
        if (empty($url_parsed['host'])) {
83✔
249
            throw new \Exception('No URL to parse in post update data source');
1✔
250
        }
251
        $link_pattern = null;
82✔
252
        $links = [];
82✔
253
        switch ($url_parsed['host']) {
82✔
254
            case 'github.com':
82✔
255
                $link_pattern = "$url/releases/tag/%s";
81✔
256
                break;
81✔
257

258
            case 'git.drupalcode.org':
4✔
259
            case 'git.drupal.org':
×
260
                if (empty($url_parsed['path'])) {
4✔
NEW
261
                    throw new \Exception('No path to parse in post update data source');
×
262
                }
263
                $project_name = str_replace('/project/', '', $url_parsed['path']);
4✔
264
                $link_pattern = "https://www.drupal.org/project/$project_name/releases/%s";
4✔
265
                break;
4✔
266

267
            default:
268
                throw new \Exception('Git URL host not supported.');
×
269
        }
270
        foreach ($data as $item) {
82✔
271
            $link = sprintf($link_pattern, $item);
1✔
272
            $links[] = sprintf('- [Release notes for tag %s](%s)', $item, $link);
1✔
273
        }
274
        return $links;
82✔
275
    }
276

277
    protected function getFetcherForUrl(string $url) : ChangelogRetriever
278
    {
279
        $token_chooser = new TokenChooser($this->slug->getUrl());
89✔
280
        $token_chooser->setUserToken($this->untouchedUserToken);
89✔
281
        $token_chooser->addTokens($this->tokens);
89✔
282
        $fetcher = $this->getFetcher();
89✔
283
        $fetcher->getRetriever()->setAuthToken($token_chooser->getChosenToken($url));
89✔
284
        return $fetcher;
89✔
285
    }
286

287
    protected function getFetcher() : ChangelogRetriever
288
    {
289
        if (!$this->fetcher instanceof ChangelogRetriever) {
89✔
290
            $cosy_factory_wrapper = new ProcessFactoryWrapper();
89✔
291
            $cosy_factory_wrapper->setExecutor($this->executer);
89✔
292
            $retriever = new DependencyRepoRetriever($cosy_factory_wrapper);
89✔
293
            $this->fetcher = new ChangelogRetriever($retriever, $cosy_factory_wrapper);
89✔
294
        }
295
        return $this->fetcher;
89✔
296
    }
297

298
    protected function closeOutdatedPrsForPackage($package_name, $current_version, Config $config, $pr_id, NamedPrs $prs_named, $default_branch)
299
    {
300
        $fake_item = (object) [
92✔
301
            'name' => $package_name,
92✔
302
            'version' => $current_version,
92✔
303
            'latest' => '',
92✔
304
        ];
92✔
305
        $branch_name_prefix = Helpers::createBranchName($fake_item, false, $config);
92✔
306
        $relevant_prs = $prs_named->getPrsFromPackage($package_name);
92✔
307
        if (empty($relevant_prs)) {
92✔
308
            $this->getLogger()->info(new Message('No direct relevant PRs found for package ' . $package_name));
92✔
309
            $relevant_prs = $prs_named->getAllPrsNamed();
92✔
310
        }
311
        foreach ($relevant_prs as $branch_name => $pr) {
92✔
312
            if (!empty($pr["base"]["ref"])) {
28✔
313
                // The base ref should be what we are actually using for merge requests.
314
                if ($pr["base"]["ref"] != $default_branch) {
3✔
315
                    continue;
1✔
316
                }
317
            }
318
            if ($pr["number"] == $pr_id) {
28✔
319
                // We really don't want to close the one we are considering as the latest one, do we?
320
                continue;
19✔
321
            }
322
            // We are just going to assume, if the number of the PR does not match. And the branch name does
323
            // indeed "match", well. Match as in it updates the exact package from the exact same version. Then
324
            // the current/recent PR will update to a newer version. Or it could also be that the branch was
325
            // created while the project was using one PR per version, and then they switched. Either way. These
326
            // two scenarios are both scenarios we want to handle in such a way that we are closing this PR that
327
            // is matching.
328
            if (strpos($branch_name, $branch_name_prefix) === false) {
14✔
329
                continue;
3✔
330
            }
331
            $comment = $this->messageFactory->getPullRequestClosedMessage($pr_id);
13✔
332
            $pr_number = $pr['number'];
13✔
333
            $this->getLogger()->log('info', new Message("Trying to close PR number $pr_number since it has been superseded by $pr_id"));
13✔
334
            try {
335
                $this->getPrClient()->closePullRequestWithComment($this->slug, $pr_number, $comment);
13✔
336
                $this->getLogger()->log('info', new Message("Successfully closed PR $pr_number"));
13✔
337
            } catch (\Throwable $e) {
×
338
                $msg = $e->getMessage();
×
339
                $this->getLogger()->log('error', new Message("Caught an exception trying to close pr $pr_number. The message was '$msg'"));
×
340
            }
341
        }
342
    }
343
}
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