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

eiriksm / cosy-composer / 13715666053

07 Mar 2025 07:12AM UTC coverage: 86.43% (+0.2%) from 86.28%
13715666053

Pull #398

github

web-flow
Merge branch 'main' into fix/refactor-up
Pull Request #398: Refactor individual update

608 of 643 new or added lines in 14 files covered. (94.56%)

3 existing lines in 1 file now uncovered.

1777 of 2056 relevant lines covered (86.43%)

43.43 hits per line

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

91.96
/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\SlugAwareTrait;
14
use eiriksm\CosyComposer\TemporaryDirectoryAwareTrait;
15
use eiriksm\CosyComposer\TokenAwareTrait;
16
use eiriksm\CosyComposer\TokenChooser;
17
use eiriksm\ViolinistMessages\ViolinistMessages;
18
use Psr\Log\LoggerAwareTrait;
19
use Psr\Log\LoggerInterface;
20
use Violinist\ChangelogFetcher\ChangelogRetriever;
21
use Violinist\ChangelogFetcher\DependencyRepoRetriever;
22
use Violinist\ComposerLockData\ComposerLockData;
23
use Violinist\Config\Config;
24
use Violinist\GitLogFormat\ChangeLogData;
25
use Violinist\ProjectData\ProjectData;
26
use Wa72\SimpleLogger\ArrayLogger;
27
use function peterpostmann\uri\parse_uri;
28

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

257
            case 'git.drupalcode.org':
1✔
NEW
258
            case 'git.drupal.org':
×
259
                $project_name = str_replace('/project/', '', $url_parsed['path']);
1✔
260
                $link_pattern = "https://www.drupal.org/project/$project_name/releases/%s";
1✔
261
                break;
1✔
262

263
            default:
NEW
264
                throw new \Exception('Git URL host not supported.');
×
265
        }
266
        foreach ($data as $item) {
75✔
NEW
267
            $link = sprintf($link_pattern, $item);
×
NEW
268
            $links[] = sprintf('- [Release notes for tag %s](%s)', $item, $link);
×
269
        }
270
        return $links;
75✔
271
    }
272

273
    protected function getFetcherForUrl(string $url) : ChangelogRetriever
274
    {
275
        $token_chooser = new TokenChooser($this->slug->getUrl());
82✔
276
        $token_chooser->setUserToken($this->untouchedUserToken);
82✔
277
        $token_chooser->addTokens($this->tokens);
82✔
278
        $fetcher = $this->getFetcher();
82✔
279
        $fetcher->getRetriever()->setAuthToken($token_chooser->getChosenToken($url));
82✔
280
        return $fetcher;
82✔
281
    }
282

283
    protected function getFetcher() : ChangelogRetriever
284
    {
285
        if (!$this->fetcher instanceof ChangelogRetriever) {
82✔
286
            $cosy_factory_wrapper = new ProcessFactoryWrapper();
82✔
287
            $cosy_factory_wrapper->setExecutor($this->executer);
82✔
288
            $retriever = new DependencyRepoRetriever($cosy_factory_wrapper);
82✔
289
            $this->fetcher = new ChangelogRetriever($retriever, $cosy_factory_wrapper);
82✔
290
        }
291
        return $this->fetcher;
82✔
292
    }
293

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