1
|
|
|
<?php // phpcs:ignore WordPress.Files.FileName.NotHyphenatedLowercase |
2
|
|
|
/** |
3
|
|
|
* "Write" command for the changelogger tool CLI. |
4
|
|
|
* |
5
|
|
|
* @package automattic/jetpack-changelogger |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
// phpcs:disable WordPress.NamingConventions.ValidVariableName |
9
|
|
|
|
10
|
|
|
namespace Automattic\Jetpack\Changelogger; |
11
|
|
|
|
12
|
|
|
use Automattic\Jetpack\Changelog\ChangeEntry; |
13
|
|
|
use Automattic\Jetpack\Changelog\Changelog; |
14
|
|
|
use InvalidArgumentException; |
15
|
|
|
use Symfony\Component\Console\Command\Command; |
16
|
|
|
use Symfony\Component\Console\Exception\MissingInputException; |
17
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
18
|
|
|
use Symfony\Component\Console\Input\InputOption; |
19
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
20
|
|
|
use Symfony\Component\Console\Question\ChoiceQuestion; |
21
|
|
|
use Symfony\Component\Console\Question\ConfirmationQuestion; |
22
|
|
|
use function Wikimedia\quietCall; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* "Write" command for the changelogger tool CLI. |
26
|
|
|
*/ |
27
|
|
|
class WriteCommand extends Command { |
28
|
|
|
|
29
|
|
|
const OK_EXIT = 0; |
30
|
|
|
const NO_CHANGE_EXIT = 1; |
31
|
|
|
const ASKED_EXIT = 2; |
32
|
|
|
const FATAL_EXIT = 3; |
33
|
|
|
const DELETE_FAILED_EXIT = 4; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* The default command name. |
37
|
|
|
* |
38
|
|
|
* @var string|null |
39
|
|
|
*/ |
40
|
|
|
protected static $defaultName = 'write'; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* The FormatterPlugin in use. |
44
|
|
|
* |
45
|
|
|
* @var FormatterPlugin |
46
|
|
|
*/ |
47
|
|
|
protected $formatter; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* The VersioningPlugin in use. |
51
|
|
|
* |
52
|
|
|
* @var VersioningPlugin |
53
|
|
|
*/ |
54
|
|
|
protected $versioning; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Whether we already asked about there being no changes. |
58
|
|
|
* |
59
|
|
|
* @var bool |
60
|
|
|
*/ |
61
|
|
|
protected $askedNoChanges = false; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* Configures the command. |
65
|
|
|
*/ |
66
|
|
|
protected function configure() { |
67
|
|
|
$this->setDescription( 'Updates the changelog from change files' ) |
68
|
|
|
->addOption( 'amend', null, InputOption::VALUE_NONE, 'Amend the latest version instead of creating a new one' ) |
69
|
|
|
->addOption( 'yes', null, InputOption::VALUE_NONE, 'Default all questions to "yes" instead of "no". Particularly useful for non-interactive mode' ) |
70
|
|
|
->addOption( 'use-version', null, InputOption::VALUE_REQUIRED, 'Use this version instead of determining the version automatically' ) |
71
|
|
|
->addOption( 'use-significance', null, InputOption::VALUE_REQUIRED, 'When determining the new version, use this significance instead of using the actual change files' ) |
72
|
|
|
->addOption( 'prerelease', 'p', InputOption::VALUE_REQUIRED, 'When determining the new version, include this prerelease suffix' ) |
73
|
|
|
->addOption( 'buildinfo', 'b', InputOption::VALUE_REQUIRED, 'When fetching the next version, include this buildinfo suffix' ) |
74
|
|
|
->addOption( 'default-first-version', null, InputOption::VALUE_NONE, 'If the changelog is currently empty, guess a "first" version instead of erroring' ) |
75
|
|
|
->addOption( 'deduplicate', null, InputOption::VALUE_REQUIRED, 'Deduplicate new changes against the last N versions', 1 ) |
76
|
|
|
->addOption( 'prologue', null, InputOption::VALUE_REQUIRED, 'Prologue text for the new changelog entry' ) |
77
|
|
|
->addOption( 'epilogue', null, InputOption::VALUE_REQUIRED, 'Epilogue text for the new changelog entry' ) |
78
|
|
|
->addOption( 'link', null, InputOption::VALUE_REQUIRED, 'Link for the new changelog entry' ) |
79
|
|
|
->setHelp( |
80
|
|
|
<<<EOF |
81
|
|
|
The <info>write</info> command adds a new changelog entry based on the changes files, and removes the changes files. |
82
|
|
|
|
83
|
|
|
Various edge cases will interactively prompt for information if possible. Use <info>--no-interaction</info> to avoid |
84
|
|
|
this, along with <info>--yes</info> if you want to proceed through all prompts instead of stopping. |
85
|
|
|
|
86
|
|
|
Exit codes are: |
87
|
|
|
|
88
|
|
|
* 0: Success. |
89
|
|
|
* 1: No changes were found, and continuing wasn't forced. |
90
|
|
|
* 2: A non-fatal error was encountered and continuing wasn't forced. |
91
|
|
|
* 3: A fatal error was encountered. |
92
|
|
|
* 4: Changelog was successfully updated, but changes files could not be removed. |
93
|
|
|
EOF |
94
|
|
|
); |
95
|
|
|
|
96
|
|
|
try { |
97
|
|
|
$this->getDefinition()->addOptions( Config::formatterPlugin()->getOptions() ); |
98
|
|
|
} catch ( \Exception $ex ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch |
99
|
|
|
// Will handle later. |
100
|
|
|
} |
101
|
|
|
try { |
102
|
|
|
$this->getDefinition()->addOptions( Config::versioningPlugin()->getOptions() ); |
103
|
|
|
} catch ( \Exception $ex ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch |
104
|
|
|
// Will handle later. |
105
|
|
|
} |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* Ask to continue. |
110
|
|
|
* |
111
|
|
|
* @param InputInterface $input InputInterface. |
112
|
|
|
* @param OutputInterface $output OutputInterface. |
113
|
|
|
* @param string $msg Situation being asked about. |
114
|
|
|
* @return bool |
115
|
|
|
*/ |
116
|
|
|
private function askToContinue( InputInterface $input, OutputInterface $output, $msg ) { |
117
|
|
|
$yes = (bool) $input->getOption( 'yes' ); |
118
|
|
|
|
119
|
|
|
if ( ! $input->isInteractive() ) { |
120
|
|
|
if ( $yes ) { |
121
|
|
|
$output->writeln( "<warning>$msg</> Continuing anyway." ); |
122
|
|
|
return true; |
123
|
|
|
} |
124
|
|
|
$output->writeln( "<error>$msg</>" ); |
125
|
|
|
return false; |
126
|
|
|
} |
127
|
|
|
try { |
128
|
|
|
$question = new ConfirmationQuestion( "$msg Proceed? " . ( $yes ? '[Y/n] ' : '[y/N] ' ), $yes ); |
129
|
|
|
return $this->getHelper( 'question' )->ask( $input, $output, $question ); |
130
|
|
|
} catch ( MissingInputException $ex ) { // @codeCoverageIgnore |
|
|
|
|
131
|
|
|
$output->writeln( 'Got EOF when attempting to query user, aborting.', OutputInterface::VERBOSITY_VERBOSE ); // @codeCoverageIgnore |
132
|
|
|
return false; // @codeCoverageIgnore |
133
|
|
|
} |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
/** |
137
|
|
|
* Load the changelog. |
138
|
|
|
* |
139
|
|
|
* @param InputInterface $input InputInterface. |
140
|
|
|
* @param OutputInterface $output OutputInterface. |
141
|
|
|
* @return Changelog|int Changelog if everything went fine, or an exit code. |
142
|
|
|
*/ |
143
|
|
|
protected function loadChangelog( InputInterface $input, OutputInterface $output ) { |
144
|
|
|
// Load changelog. |
145
|
|
|
$file = Config::changelogFile(); |
146
|
|
|
if ( ! file_exists( $file ) ) { |
147
|
|
|
if ( ! $this->askToContinue( $input, $output, "Changelog file $file does not exist!" ) ) { |
148
|
|
|
return self::ASKED_EXIT; |
149
|
|
|
} |
150
|
|
|
$changelog = new Changelog(); |
151
|
|
|
} else { |
152
|
|
|
$output->writeln( "Reading changelog from $file...", OutputInterface::VERBOSITY_DEBUG ); |
153
|
|
|
Utils::error_clear_last(); |
154
|
|
|
$contents = quietCall( 'file_get_contents', $file ); |
155
|
|
|
// @codeCoverageIgnoreStart |
156
|
|
View Code Duplication |
if ( ! is_string( $contents ) ) { |
157
|
|
|
$err = error_get_last(); |
158
|
|
|
$output->writeln( "<error>Failed to read $file: {$err['message']}</>" ); |
159
|
|
|
return self::FATAL_EXIT; |
160
|
|
|
} |
161
|
|
|
// @codeCoverageIgnoreEnd |
162
|
|
|
try { |
163
|
|
|
$changelog = $this->formatter->parse( $contents ); |
164
|
|
|
} catch ( \Exception $ex ) { |
165
|
|
|
$output->writeln( "<error>Failed to parse changelog: {$ex->getMessage()}</>" ); |
166
|
|
|
return self::FATAL_EXIT; |
167
|
|
|
} |
168
|
|
|
} |
169
|
|
|
return $changelog; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Add the entry to the changelog. |
174
|
|
|
* |
175
|
|
|
* @param InputInterface $input InputInterface. |
176
|
|
|
* @param OutputInterface $output OutputInterface. |
177
|
|
|
* @param Changelog $changelog Changelog. |
178
|
|
|
* @param string $version Version. |
179
|
|
|
* @param ChangeEntry[] $changes Changes. |
180
|
|
|
* @return int |
181
|
|
|
*/ |
182
|
|
|
protected function addEntry( InputInterface $input, OutputInterface $output, Changelog $changelog, $version, array $changes ) { |
183
|
|
|
$output->writeln( 'Creating new changelog entry.', OutputInterface::VERBOSITY_DEBUG ); |
184
|
|
|
$data = array( |
185
|
|
|
'prologue' => (string) $input->getOption( 'prologue' ), |
186
|
|
|
'epilogue' => (string) $input->getOption( 'epilogue' ), |
187
|
|
|
'link' => $input->getOption( 'link' ), |
188
|
|
|
'changes' => $changes, |
189
|
|
|
); |
190
|
|
|
if ( null === $data['link'] && $changelog->getLatestEntry() ) { |
191
|
|
|
$data['link'] = Config::link( $changelog->getLatestEntry()->getVersion(), $version ); |
192
|
|
|
} |
193
|
|
|
try { |
194
|
|
|
$changelog->addEntry( $this->formatter->newChangelogEntry( $version, $data ) ); |
195
|
|
|
} catch ( InvalidArgumentException $ex ) { |
196
|
|
|
$output->writeln( "<error>Failed to create changelog entry: {$ex->getMessage()}</>" ); |
197
|
|
|
return self::FATAL_EXIT; |
198
|
|
|
} |
199
|
|
|
return self::OK_EXIT; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* Write the changelog. |
204
|
|
|
* |
205
|
|
|
* @param InputInterface $input InputInterface. |
206
|
|
|
* @param OutputInterface $output OutputInterface. |
207
|
|
|
* @param Changelog $changelog Changelog. |
208
|
|
|
* @return int |
209
|
|
|
*/ |
210
|
|
|
protected function writeChangelog( InputInterface $input, OutputInterface $output, Changelog $changelog ) { |
211
|
|
|
$file = Config::changelogFile(); |
212
|
|
|
$output->writeln( "Writing changelog to $file...", OutputInterface::VERBOSITY_DEBUG ); |
213
|
|
|
try { |
214
|
|
|
$contents = $this->formatter->format( $changelog ); |
215
|
|
|
} catch ( InvalidArgumentException $ex ) { |
216
|
|
|
$output->writeln( "<error>Failed to write the changelog: {$ex->getMessage()}</>" ); |
217
|
|
|
return self::FATAL_EXIT; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
Utils::error_clear_last(); |
221
|
|
|
$ok = quietCall( 'file_put_contents', $file, $contents ); |
222
|
|
|
if ( strlen( $contents ) !== $ok ) { |
223
|
|
|
$err = error_get_last(); |
224
|
|
|
$output->writeln( "<error>Failed to write $file: {$err['message']}</>" ); |
225
|
|
|
return self::FATAL_EXIT; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
return self::OK_EXIT; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* Delete the change files. |
233
|
|
|
* |
234
|
|
|
* @param InputInterface $input InputInterface. |
235
|
|
|
* @param OutputInterface $output OutputInterface. |
236
|
|
|
* @param array $files Files returned from `loadChanges()`. |
237
|
|
|
* @return int |
238
|
|
|
*/ |
239
|
|
|
protected function deleteChanges( InputInterface $input, OutputInterface $output, array $files ) { |
240
|
|
|
$dir = Config::changesDir(); |
241
|
|
|
$ret = self::OK_EXIT; |
242
|
|
|
foreach ( $files as $name => $flag ) { |
243
|
|
|
if ( $flag >= 2 ) { |
244
|
|
|
continue; |
245
|
|
|
} |
246
|
|
|
Utils::error_clear_last(); |
247
|
|
|
$ok = quietCall( 'unlink', $dir . DIRECTORY_SEPARATOR . $name ); |
248
|
|
|
if ( $ok ) { |
249
|
|
|
$output->writeln( "Deleted change file $name.", OutputInterface::VERBOSITY_DEBUG ); |
250
|
|
|
} else { |
251
|
|
|
$err = error_get_last(); |
252
|
|
|
$output->writeln( "<warning>Failed to delete $name: {$err['message']}" ); |
253
|
|
|
$ret = self::DELETE_FAILED_EXIT; |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
return $ret; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Load the changes. |
261
|
|
|
* |
262
|
|
|
* @param InputInterface $input InputInterface. |
263
|
|
|
* @param OutputInterface $output OutputInterface. |
264
|
|
|
* @return array Array of [ $code, $changes, $files ]. |
265
|
|
|
*/ |
266
|
|
|
protected function loadChanges( InputInterface $input, OutputInterface $output ) { |
267
|
|
|
$dir = Config::changesDir(); |
268
|
|
|
if ( ! is_dir( $dir ) ) { |
269
|
|
|
$this->askedNoChanges = true; |
270
|
|
View Code Duplication |
if ( ! $this->askToContinue( $input, $output, 'Changes directory does not exist, so there are no changes to write!' ) ) { |
271
|
|
|
return array( self::NO_CHANGE_EXIT, null, null ); |
272
|
|
|
} |
273
|
|
|
return array( self::OK_EXIT, array(), array() ); |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
$output->writeln( "Reading changes from $dir...", OutputInterface::VERBOSITY_DEBUG ); |
277
|
|
|
$files = null; // Make phpcs happy. |
278
|
|
|
$changes = Utils::loadAllChanges( $dir, Config::types(), $this->formatter, $output, $files ); |
279
|
|
|
$max = $files ? max( $files ) : 0; |
280
|
|
|
if ( $max > 0 && ! $this->askToContinue( $input, $output, ( $max > 1 ? 'Errors' : 'Warnings' ) . ' were encountered while reading changes!' ) ) { |
281
|
|
|
return array( self::ASKED_EXIT, null, null ); |
282
|
|
|
} |
283
|
|
|
if ( ! $changes && ! $this->askedNoChanges ) { |
|
|
|
|
284
|
|
|
$this->askedNoChanges = true; |
285
|
|
View Code Duplication |
if ( ! $this->askToContinue( $input, $output, 'No changes were found!' ) ) { |
286
|
|
|
return array( self::NO_CHANGE_EXIT, null, null ); |
287
|
|
|
} |
288
|
|
|
} |
289
|
|
|
return array( self::OK_EXIT, $changes, $files ); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* Deduplicate changes. |
294
|
|
|
* |
295
|
|
|
* @param InputInterface $input InputInterface. |
296
|
|
|
* @param OutputInterface $output OutputInterface. |
297
|
|
|
* @param Changelog $changelog Changelog. |
298
|
|
|
* @param ChangeEntry[] $changes Changes. |
299
|
|
|
* @return int |
300
|
|
|
*/ |
301
|
|
|
protected function deduplicateChanges( InputInterface $input, OutputInterface $output, Changelog $changelog, array &$changes ) { |
302
|
|
|
// Deduplicate changes. |
303
|
|
|
if ( ! $changes ) { |
|
|
|
|
304
|
|
|
$output->writeln( 'Skipping deduplication, there are no changes.', OutputInterface::VERBOSITY_DEBUG ); |
305
|
|
|
return self::OK_EXIT; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
$depth = (int) $input->getOption( 'deduplicate' ); |
309
|
|
|
if ( 0 === $depth ) { |
310
|
|
|
$output->writeln( 'Skipping deduplication, --deduplicate is 0.', OutputInterface::VERBOSITY_DEBUG ); |
311
|
|
|
return self::OK_EXIT; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
$output->writeln( "Deduplicating changes from the last $depth version(s)...", OutputInterface::VERBOSITY_DEBUG ); |
315
|
|
|
$dedup = array(); |
316
|
|
|
foreach ( array_slice( $changelog->getEntries(), 0, $depth ) as $entry ) { |
317
|
|
|
foreach ( $entry->getChanges() as $change ) { |
318
|
|
|
$dedup[ $change->getContent() ] = true; |
319
|
|
|
} |
320
|
|
|
} |
321
|
|
|
unset( $dedup[''] ); |
322
|
|
|
if ( $dedup ) { |
|
|
|
|
323
|
|
|
$changes = array_filter( |
324
|
|
|
$changes, |
325
|
|
|
function ( $change, $name ) use ( $dedup, $output ) { |
326
|
|
|
if ( isset( $dedup[ $change->getContent() ] ) ) { |
327
|
|
|
$output->writeln( "Found duplicate change in $name.", OutputInterface::VERBOSITY_DEBUG ); |
328
|
|
|
return false; |
329
|
|
|
} |
330
|
|
|
return true; |
331
|
|
|
}, |
332
|
|
|
ARRAY_FILTER_USE_BOTH |
333
|
|
|
); |
334
|
|
|
} |
335
|
|
View Code Duplication |
if ( ! $changes && ! $this->askedNoChanges ) { |
|
|
|
|
336
|
|
|
$this->askedNoChanges = true; |
337
|
|
|
if ( ! $this->askToContinue( $input, $output, 'All changes were duplicates.' ) ) { |
338
|
|
|
return self::NO_CHANGE_EXIT; |
339
|
|
|
} |
340
|
|
|
} |
341
|
|
|
return self::OK_EXIT; |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* Check whether any changes have content. |
346
|
|
|
* |
347
|
|
|
* @param InputInterface $input InputInterface. |
348
|
|
|
* @param OutputInterface $output OutputInterface. |
349
|
|
|
* @param ChangeEntry[] $changes Changes. |
350
|
|
|
* @return bool |
351
|
|
|
*/ |
352
|
|
|
protected function doChangesHaveContent( InputInterface $input, OutputInterface $output, array $changes ) { |
353
|
|
|
$output->writeln( 'Checking if any changes have content...', OutputInterface::VERBOSITY_DEBUG ); |
354
|
|
|
foreach ( $changes as $name => $change ) { |
355
|
|
|
if ( $change->getContent() !== '' ) { |
356
|
|
|
$output->writeln( "Yes, $name has content.", OutputInterface::VERBOSITY_DEBUG ); |
357
|
|
|
return true; |
358
|
|
|
} |
359
|
|
|
} |
360
|
|
|
return false; |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
/** |
364
|
|
|
* Apply --amend if applicable. |
365
|
|
|
* |
366
|
|
|
* @param InputInterface $input InputInterface. |
367
|
|
|
* @param OutputInterface $output OutputInterface. |
368
|
|
|
* @param Changelog $changelog Changelog. |
369
|
|
|
* @param ChangeEntry[] $changes Changes. |
370
|
|
|
* @param string|null $amendedVersion Set to indicate the source version of the amend, if any. |
371
|
|
|
* @return int |
372
|
|
|
*/ |
373
|
|
|
protected function doAmendChanges( InputInterface $input, OutputInterface $output, Changelog $changelog, array &$changes, &$amendedVersion = null ) { |
374
|
|
|
$amendedVersion = null; |
375
|
|
|
if ( $input->getOption( 'amend' ) ) { |
376
|
|
|
$entries = $changelog->getEntries(); |
377
|
|
|
if ( $entries ) { |
|
|
|
|
378
|
|
|
$latest = array_shift( $entries ); |
379
|
|
|
$changelog->setEntries( $entries ); |
380
|
|
|
$amendedVersion = $latest->getVersion(); |
381
|
|
|
$changes = array_merge( $latest->getChanges(), array_values( $changes ) ); |
382
|
|
|
$output->writeln( "Removing changes for $amendedVersion from changelog for --amend.", OutputInterface::VERBOSITY_DEBUG ); |
383
|
|
|
|
384
|
|
|
if ( $input->getOption( 'prologue' ) === null ) { |
385
|
|
|
$input->setOption( 'prologue', $latest->getPrologue() ); |
386
|
|
|
} |
387
|
|
|
if ( $input->getOption( 'epilogue' ) === null ) { |
388
|
|
|
$input->setOption( 'epilogue', $latest->getEpilogue() ); |
389
|
|
|
} |
390
|
|
|
if ( $input->getOption( 'link' ) === null ) { |
391
|
|
|
$input->setOption( 'link', $latest->getLink() ); |
392
|
|
|
} |
393
|
|
|
} else { |
394
|
|
|
$output->writeln( 'No version to amend, ignoring --amend.', OutputInterface::VERBOSITY_DEBUG ); |
395
|
|
|
} |
396
|
|
|
} |
397
|
|
|
return self::OK_EXIT; |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
/** |
401
|
|
|
* Sort changes. |
402
|
|
|
* |
403
|
|
|
* @param ChangeEntry[] $changes Changes. |
404
|
|
|
* @return array |
405
|
|
|
*/ |
406
|
|
|
protected function sortChanges( array $changes ) { |
407
|
|
|
$sortConfig = array( |
408
|
|
|
'ordering' => Config::ordering(), |
409
|
|
|
'knownSubheadings' => Config::types(), |
410
|
|
|
); |
411
|
|
|
usort( |
412
|
|
|
$changes, |
413
|
|
|
function ( $a, $b ) use ( $sortConfig, $changes ) { |
414
|
|
|
$ret = ChangeEntry::compare( $a, $b, $sortConfig ); |
415
|
|
|
if ( 0 === $ret ) { |
416
|
|
|
// Stability. |
417
|
|
|
$ret = array_search( $a, $changes, true ) - array_search( $b, $changes, true ); |
418
|
|
|
} |
419
|
|
|
return $ret; |
420
|
|
|
} |
421
|
|
|
); |
422
|
|
|
return $changes; |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
/** |
426
|
|
|
* Get the version from the command line. |
427
|
|
|
* |
428
|
|
|
* @param InputInterface $input InputInterface. |
429
|
|
|
* @param OutputInterface $output OutputInterface. |
430
|
|
|
* @param Changelog $changelog Changelog. |
431
|
|
|
* @return string|int New version, or int on error. |
432
|
|
|
*/ |
433
|
|
|
protected function getUseVersion( InputInterface $input, OutputInterface $output, Changelog $changelog ) { |
434
|
|
|
$version = $input->getOption( 'use-version' ); |
435
|
|
|
$output->writeln( "Using version $version from command line.", OutputInterface::VERBOSITY_DEBUG ); |
436
|
|
|
|
437
|
|
|
// Normalize it? |
438
|
|
|
try { |
439
|
|
|
$nversion = $this->versioning->normalizeVersion( $version ); |
440
|
|
|
} catch ( InvalidArgumentException $ex ) { |
441
|
|
|
$nversion = $version; |
442
|
|
|
$output->writeln( "<error>Invalid --use-version: {$ex->getMessage()}</>" ); |
443
|
|
|
if ( ! $this->askToContinue( $input, $output, 'The specified version is not valid. This may cause problems in the future!' ) ) { |
444
|
|
|
return self::ASKED_EXIT; |
445
|
|
|
} |
446
|
|
|
} |
447
|
|
|
if ( $version !== $nversion ) { |
448
|
|
|
if ( ! $input->isInteractive() ) { |
449
|
|
|
if ( ! $this->askToContinue( $input, $output, "The supplied version $version is not normalized, it should be $nversion." ) ) { |
450
|
|
|
return self::ASKED_EXIT; |
451
|
|
|
} |
452
|
|
|
} else { |
453
|
|
|
try { |
454
|
|
|
$question = new ChoiceQuestion( |
455
|
|
|
"The supplied version $version is not normalized.", |
456
|
|
|
array( |
457
|
|
|
'proceed' => "Proceed with $version", |
458
|
|
|
'normalize' => "Normalize to $nversion", |
459
|
|
|
'abort' => 'Abort', |
460
|
|
|
), |
461
|
|
|
$input->getOption( 'yes' ) ? 'proceed' : 'abort' |
462
|
|
|
); |
463
|
|
|
switch ( $this->getHelper( 'question' )->ask( $input, $output, $question ) ) { |
464
|
|
|
case 'proceed': // @codeCoverageIgnore |
465
|
|
|
break; |
466
|
|
|
case 'normalize': // @codeCoverageIgnore |
467
|
|
|
$output->writeln( "Normalizing $version to $nversion.", OutputInterface::VERBOSITY_DEBUG ); |
468
|
|
|
$version = $nversion; |
469
|
|
|
break; |
470
|
|
|
default: |
471
|
|
|
return self::ASKED_EXIT; |
472
|
|
|
} |
473
|
|
|
} catch ( MissingInputException $ex ) { // @codeCoverageIgnore |
|
|
|
|
474
|
|
|
$output->writeln( 'Got EOF when attempting to query user, aborting.', OutputInterface::VERBOSITY_VERBOSE ); // @codeCoverageIgnore |
475
|
|
|
return self::ASKED_EXIT; // @codeCoverageIgnore |
476
|
|
|
} |
477
|
|
|
} |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
// Check that it's newer than the current version. |
481
|
|
|
$latest = $changelog->getLatestEntry(); |
482
|
|
|
if ( $latest ) { |
483
|
|
|
$curver = $latest->getVersion(); |
484
|
|
|
try { |
485
|
|
|
$cmp = $this->versioning->compareVersions( $version, $curver ); |
486
|
|
|
} catch ( InvalidArgumentException $ex ) { |
487
|
|
|
$output->writeln( "Cannot compare $version with $curver: {$ex->getMessage()}", OutputInterface::VERBOSITY_DEBUG ); |
488
|
|
|
$cmp = 1; |
489
|
|
|
} |
490
|
|
View Code Duplication |
if ( $cmp < 0 && ! $this->askToContinue( $input, $output, "The most recent version in the changelog is $curver, which comes after $version." ) ) { |
491
|
|
|
return self::ASKED_EXIT; |
492
|
|
|
} |
493
|
|
View Code Duplication |
if ( 0 === $cmp && ! $this->askToContinue( $input, $output, "The most recent version in the changelog is $curver, which is equivalent to $version." ) ) { |
494
|
|
|
return self::ASKED_EXIT; |
495
|
|
|
} |
496
|
|
|
} |
497
|
|
|
return $version; |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
/** |
501
|
|
|
* Determine the next version. |
502
|
|
|
* |
503
|
|
|
* @param InputInterface $input InputInterface. |
504
|
|
|
* @param OutputInterface $output OutputInterface. |
505
|
|
|
* @param Changelog $changelog Changelog. |
506
|
|
|
* @param ChangeEntry[] $changes Changes. |
507
|
|
|
* @param string|null $amendedVersion The source version of the amend, if any. |
508
|
|
|
* @return string|int New version, or int on error. |
509
|
|
|
*/ |
510
|
|
|
protected function nextVersion( InputInterface $input, OutputInterface $output, Changelog $changelog, array $changes, $amendedVersion ) { |
511
|
|
|
$extra = array_filter( |
512
|
|
|
array( |
513
|
|
|
'prerelease' => $input->getOption( 'prerelease' ), |
514
|
|
|
'buildinfo' => $input->getOption( 'buildinfo' ), |
515
|
|
|
) |
516
|
|
|
); |
517
|
|
|
|
518
|
|
|
// Is there a version in the changelog? |
519
|
|
|
$latest = $changelog->getLatestEntry(); |
520
|
|
|
if ( ! $latest ) { |
521
|
|
|
if ( null !== $amendedVersion ) { |
522
|
|
|
$output->writeln( "Amending earliest version, reusing version $amendedVersion...", OutputInterface::VERBOSITY_DEBUG ); |
523
|
|
|
return $amendedVersion; |
524
|
|
|
} elseif ( $input->getOption( 'default-first-version' ) ) { |
525
|
|
|
return $this->versioning->firstVersion( $extra ); |
526
|
|
|
} else { |
527
|
|
|
$output->writeln( '<error>Changelog file contains no entries! Use --use-version to specify the initial version.</>' ); |
528
|
|
|
return self::FATAL_EXIT; |
529
|
|
|
} |
530
|
|
|
} |
531
|
|
|
|
532
|
|
|
$output->writeln( "Latest version from changelog is {$latest->getVersion()}.", OutputInterface::VERBOSITY_DEBUG ); |
533
|
|
|
|
534
|
|
|
// If they overrode the significance, use that. Otherwise use `$changes`. |
535
|
|
|
if ( $input->getOption( 'use-significance' ) ) { |
536
|
|
|
try { |
537
|
|
|
$verchanges = array( |
538
|
|
|
$this->formatter->newChangeEntry( |
539
|
|
|
array( |
540
|
|
|
'significance' => $input->getOption( 'use-significance' ), |
541
|
|
|
'content' => 'Dummy', |
542
|
|
|
) |
543
|
|
|
), |
544
|
|
|
); |
545
|
|
|
} catch ( \Exception $ex ) { |
546
|
|
|
$output->writeln( "<error>{$ex->getMessage()}</>" ); |
547
|
|
|
return self::FATAL_EXIT; |
548
|
|
|
} |
549
|
|
|
} else { |
550
|
|
|
$verchanges = $changes; |
551
|
|
|
} |
552
|
|
|
|
553
|
|
|
// Get the next version from the versioning plugin. |
554
|
|
|
try { |
555
|
|
|
$version = $this->versioning->nextVersion( $latest->getVersion(), $verchanges, $extra ); |
556
|
|
|
} catch ( InvalidArgumentException $ex ) { |
557
|
|
|
// Was it the version from the changelog that made it fail, or something else? |
558
|
|
|
try { |
559
|
|
|
$this->versioning->normalizeVersion( $latest->getVersion() ); |
560
|
|
|
$output->writeln( "<error>Failed to determine new version: {$ex->getMessage()}</>" ); |
561
|
|
|
} catch ( InvalidArgumentException $ex2 ) { |
562
|
|
|
$output->writeln( "<error>Changelog file contains invalid version {$latest->getVersion()}! Use --use-version to specify the new version.</>" ); |
563
|
|
|
} |
564
|
|
|
return self::FATAL_EXIT; |
565
|
|
|
} |
566
|
|
|
$output->writeln( "Next version is {$version}.", OutputInterface::VERBOSITY_DEBUG ); |
567
|
|
|
|
568
|
|
|
// When amending, if the next version turns out to be before the amended version, use the amended version. |
569
|
|
|
try { |
570
|
|
|
if ( null !== $amendedVersion && $this->versioning->compareVersions( $amendedVersion, $version ) > 0 ) { |
571
|
|
|
$output->writeln( "Amended version $amendedVersion is later, using that instead.", OutputInterface::VERBOSITY_DEBUG ); |
572
|
|
|
$version = $amendedVersion; |
573
|
|
|
} |
574
|
|
|
} catch ( InvalidArgumentException $ex ) { |
575
|
|
|
$output->writeln( "Amended version $amendedVersion is was not valid. Hope it wasn't supposed to be later.", OutputInterface::VERBOSITY_DEBUG ); |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
return $version; |
579
|
|
|
} |
580
|
|
|
|
581
|
|
|
/** |
582
|
|
|
* Executes the command. |
583
|
|
|
* |
584
|
|
|
* @param InputInterface $input InputInterface. |
585
|
|
|
* @param OutputInterface $output OutputInterface. |
586
|
|
|
* @return int 0 If everything went fine, or an exit code. |
587
|
|
|
*/ |
588
|
|
|
protected function execute( InputInterface $input, OutputInterface $output ) { |
589
|
|
|
try { |
590
|
|
|
$this->formatter = Config::formatterPlugin(); |
|
|
|
|
591
|
|
|
$this->formatter->setIO( $input, $output ); |
592
|
|
|
$this->versioning = Config::versioningPlugin(); |
|
|
|
|
593
|
|
|
$this->versioning->setIO( $input, $output ); |
594
|
|
|
} catch ( \Exception $ex ) { |
595
|
|
|
$output->writeln( "<error>{$ex->getMessage()}</>" ); |
596
|
|
|
return self::FATAL_EXIT; |
597
|
|
|
} |
598
|
|
|
$this->askedNoChanges = false; |
599
|
|
|
|
600
|
|
|
// Get the changelog. |
601
|
|
|
$changelog = $this->loadChangelog( $input, $output ); |
602
|
|
|
if ( is_int( $changelog ) ) { |
603
|
|
|
return $changelog; |
604
|
|
|
} |
605
|
|
|
|
606
|
|
|
// Get the changes. |
607
|
|
|
list( $ret, $changes, $files ) = $this->loadChanges( $input, $output, $changelog ); |
608
|
|
|
if ( self::OK_EXIT !== $ret ) { |
609
|
|
|
return $ret; |
610
|
|
|
} |
611
|
|
|
$ret = $this->deduplicateChanges( $input, $output, $changelog, $changes ); |
612
|
|
|
if ( self::OK_EXIT !== $ret ) { |
613
|
|
|
return $ret; |
614
|
|
|
} |
615
|
|
|
$anyChangesWithContent = $this->doChangesHaveContent( $input, $output, $changes ); |
616
|
|
View Code Duplication |
if ( ! $anyChangesWithContent && ! $this->askedNoChanges ) { |
617
|
|
|
$this->askedNoChanges = true; |
618
|
|
|
if ( ! $this->askToContinue( $input, $output, 'There are no changes with content for this write.' ) ) { |
619
|
|
|
return self::NO_CHANGE_EXIT; |
620
|
|
|
} |
621
|
|
|
} |
622
|
|
|
$amendedVersion = null; // Make phpcs happy. |
623
|
|
|
$ret = $this->doAmendChanges( $input, $output, $changelog, $changes, $amendedVersion ); |
624
|
|
|
if ( self::OK_EXIT !== $ret ) { |
625
|
|
|
return $ret; // @codeCoverageIgnore |
626
|
|
|
} |
627
|
|
|
$changes = $this->sortChanges( $changes ); |
628
|
|
|
|
629
|
|
|
// Determine next version. |
630
|
|
|
if ( $input->getOption( 'use-version' ) !== null ) { |
631
|
|
|
$version = $this->getUseVersion( $input, $output, $changelog ); |
632
|
|
|
} else { |
633
|
|
|
$version = $this->nextVersion( $input, $output, $changelog, $changes, $amendedVersion ); |
634
|
|
|
} |
635
|
|
|
if ( is_int( $version ) ) { |
636
|
|
|
return $version; |
637
|
|
|
} |
638
|
|
|
|
639
|
|
|
// Add the new changelog entry. |
640
|
|
|
$ret = $this->addEntry( $input, $output, $changelog, $version, $changes ); |
641
|
|
|
if ( self::OK_EXIT !== $ret ) { |
642
|
|
|
return $ret; |
643
|
|
|
} |
644
|
|
|
|
645
|
|
|
// Write the changelog. |
646
|
|
|
$ret = $this->writeChangelog( $input, $output, $changelog ); |
647
|
|
|
if ( self::OK_EXIT !== $ret ) { |
648
|
|
|
return $ret; |
649
|
|
|
} |
650
|
|
|
|
651
|
|
|
// Delete change files and return. |
652
|
|
|
return $this->deleteChanges( $input, $output, $files ); |
653
|
|
|
} |
654
|
|
|
} |
655
|
|
|
|
Scrutinizer analyzes your
composer.json
/composer.lock
file if available to determine the classes, and functions that are defined by your dependencies.It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.