Completed
Push — add/changelog-tooling ( b2784f...e205c1 )
by
unknown
109:57 queued 100:19
created

WriteCommand::loadChanges()   B

Complexity

Conditions 10
Paths 10

Size

Total Lines 24

Duplication

Lines 6
Ratio 25 %

Importance

Changes 0
Metric Value
cc 10
nc 10
nop 2
dl 6
loc 24
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php // 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( 'deduplicate', null, InputOption::VALUE_REQUIRED, 'Deduplicate new changes against the last N versions', 1 )
75
			->addOption( 'prologue', null, InputOption::VALUE_REQUIRED, 'Prologue text for the new changelog entry' )
76
			->addOption( 'epilogue', null, InputOption::VALUE_REQUIRED, 'Epilogue text for the new changelog entry' )
77
			->addOption( 'link', null, InputOption::VALUE_REQUIRED, 'Link for the new changelog entry' )
78
			->setHelp(
79
				<<<EOF
80
The <info>write</info> command adds a new changelog entry based on the changes files, and removes the changes files.
81
82
Various edge cases will interactively prompt for information if possible. Use <info>--no-interaction</info> to avoid
83
this, along with <info>--yes</info> if you want to proceed through all prompts instead of stopping.
84
85
Exit codes are:
86
87
* 0: Success.
88
* 1: No changes were found, and continuing wasn't forced.
89
* 2: A non-fatal error was encountered and continuing wasn't forced.
90
* 3: A fatal error was encountered.
91
* 4: Changelog was successfully updated, but changes files could not be removed.
92
EOF
93
			);
94
95
		try {
96
			$this->getDefinition()->addOptions( Config::formatterPlugin()->getOptions() );
97
		} catch ( \Exception $ex ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
98
			// Will handle later.
99
		}
100
		try {
101
			$this->getDefinition()->addOptions( Config::versioningPlugin()->getOptions() );
102
		} catch ( \Exception $ex ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
103
			// Will handle later.
104
		}
105
	}
106
107
	/**
108
	 * Ask to continue.
109
	 *
110
	 * @param InputInterface  $input InputInterface.
111
	 * @param OutputInterface $output OutputInterface.
112
	 * @param string          $msg Situation being asked about.
113
	 * @return bool
114
	 */
115
	private function askToContinue( InputInterface $input, OutputInterface $output, $msg ) {
116
		$yes = (bool) $input->getOption( 'yes' );
117
118
		if ( ! $input->isInteractive() ) {
119
			if ( $yes ) {
120
				$output->writeln( "<warning>$msg</> Continuing anyway." );
121
				return true;
122
			}
123
			$output->writeln( "<error>$msg</>" );
124
			return false;
125
		}
126
		try {
127
			$question = new ConfirmationQuestion( "$msg Proceed? " . ( $yes ? '[Y/n] ' : '[y/N] ' ), $yes );
128
			return $this->getHelper( 'question' )->ask( $input, $output, $question );
129
		} catch ( MissingInputException $ex ) { // @codeCoverageIgnore
0 ignored issues
show
Bug introduced by
The class Symfony\Component\Consol...n\MissingInputException does not exist. Did you forget a USE statement, or did you not list all dependencies?

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.

Loading history...
130
			$output->writeln( 'Got EOF when attempting to query user, aborting.', OutputInterface::VERBOSITY_VERBOSE ); // @codeCoverageIgnore
131
			return false; // @codeCoverageIgnore
132
		}
133
	}
134
135
	/**
136
	 * Load the changelog.
137
	 *
138
	 * @param InputInterface  $input InputInterface.
139
	 * @param OutputInterface $output OutputInterface.
140
	 * @return Changelog|int Changelog if everything went fine, or an exit code.
141
	 */
142
	protected function loadChangelog( InputInterface $input, OutputInterface $output ) {
143
		// Load changelog.
144
		$file = Config::changelogFile();
145
		if ( ! file_exists( $file ) ) {
146
			if ( ! $this->askToContinue( $input, $output, "Changelog file $file does not exist!" ) ) {
147
				return self::ASKED_EXIT;
148
			}
149
			$changelog = new Changelog();
150
		} else {
151
			$output->writeln( "Reading changelog from $file...", OutputInterface::VERBOSITY_DEBUG );
152
			Utils::error_clear_last();
153
			$contents = quietCall( 'file_get_contents', $file );
154
			// @codeCoverageIgnoreStart
155 View Code Duplication
			if ( ! is_string( $contents ) ) {
156
				$err = error_get_last();
157
				$output->writeln( "<error>Failed to read $file: {$err['message']}</>" );
158
				return self::FATAL_EXIT;
159
			}
160
			// @codeCoverageIgnoreEnd
161
			try {
162
				$changelog = $this->formatter->parse( $contents );
163
			} catch ( \Exception $ex ) {
164
				$output->writeln( "<error>Failed to parse changelog: {$ex->getMessage()}</>" );
165
				return self::FATAL_EXIT;
166
			}
167
		}
168
		return $changelog;
169
	}
170
171
	/**
172
	 * Add the entry to the changelog.
173
	 *
174
	 * @param InputInterface  $input InputInterface.
175
	 * @param OutputInterface $output OutputInterface.
176
	 * @param Changelog       $changelog Changelog.
177
	 * @param string          $version Version.
178
	 * @param ChangeEntry[]   $changes Changes.
179
	 * @return int
180
	 */
181
	protected function addEntry( InputInterface $input, OutputInterface $output, Changelog $changelog, $version, array $changes ) {
182
		$output->writeln( 'Creating new changelog entry.', OutputInterface::VERBOSITY_DEBUG );
183
		$data = array(
184
			'prologue' => (string) $input->getOption( 'prologue' ),
185
			'epilogue' => (string) $input->getOption( 'epilogue' ),
186
			'link'     => $input->getOption( 'link' ),
187
			'changes'  => $changes,
188
		);
189
		if ( null === $data['link'] && $changelog->getLatestEntry() ) {
190
			$data['link'] = Config::link( $changelog->getLatestEntry()->getVersion(), $version );
191
		}
192
		try {
193
			$changelog->addEntry( $this->formatter->newChangelogEntry( $version, $data ) );
194
		} catch ( InvalidArgumentException $ex ) {
195
			$output->writeln( "<error>Failed to create changelog entry: {$ex->getMessage()}</>" );
196
			return self::FATAL_EXIT;
197
		}
198
		return self::OK_EXIT;
199
	}
200
201
	/**
202
	 * Write the changelog.
203
	 *
204
	 * @param InputInterface  $input InputInterface.
205
	 * @param OutputInterface $output OutputInterface.
206
	 * @param Changelog       $changelog Changelog.
207
	 * @return int
208
	 */
209
	protected function writeChangelog( InputInterface $input, OutputInterface $output, Changelog $changelog ) {
210
		$file = Config::changelogFile();
211
		$output->writeln( "Writing changelog to $file...", OutputInterface::VERBOSITY_DEBUG );
212
		try {
213
			$contents = $this->formatter->format( $changelog );
214
		} catch ( InvalidArgumentException $ex ) {
215
			$output->writeln( "<error>Failed to write the changelog: {$ex->getMessage()}</>" );
216
			return self::FATAL_EXIT;
217
		}
218
219
		Utils::error_clear_last();
220
		$ok = quietCall( 'file_put_contents', $file, $contents );
221
		if ( strlen( $contents ) !== $ok ) {
222
			$err = error_get_last();
223
			$output->writeln( "<error>Failed to write $file: {$err['message']}</>" );
224
			return self::FATAL_EXIT;
225
		}
226
227
		return self::OK_EXIT;
228
	}
229
230
	/**
231
	 * Delete the change files.
232
	 *
233
	 * @param InputInterface  $input InputInterface.
234
	 * @param OutputInterface $output OutputInterface.
235
	 * @param array           $files Files returned from `loadChanges()`.
236
	 * @return int
237
	 */
238
	protected function deleteChanges( InputInterface $input, OutputInterface $output, array $files ) {
239
		$dir = Config::changesDir();
240
		$ret = self::OK_EXIT;
241
		foreach ( $files as $name => $flag ) {
242
			if ( $flag >= 2 ) {
243
				continue;
244
			}
245
			Utils::error_clear_last();
246
			$ok = quietCall( 'unlink', $dir . DIRECTORY_SEPARATOR . $name );
247
			if ( $ok ) {
248
				$output->writeln( "Deleted change file $name.", OutputInterface::VERBOSITY_DEBUG );
249
			} else {
250
				$err = error_get_last();
251
				$output->writeln( "<warning>Failed to delete $name: {$err['message']}" );
252
				$ret = self::DELETE_FAILED_EXIT;
253
			}
254
		}
255
		return $ret;
256
	}
257
258
	/**
259
	 * Load the changes.
260
	 *
261
	 * @param InputInterface  $input InputInterface.
262
	 * @param OutputInterface $output OutputInterface.
263
	 * @return array Array of [ $code, $changes, $files ].
264
	 */
265
	protected function loadChanges( InputInterface $input, OutputInterface $output ) {
266
		$dir = Config::changesDir();
267
		if ( ! is_dir( $dir ) ) {
268 View Code Duplication
			if ( ! $this->askToContinue( $input, $output, 'Changes directory does not exist!' ) ) {
269
				return array( self::ASKED_EXIT, null, null );
270
			}
271
			return array( self::OK_EXIT, array(), array() );
272
		}
273
274
		$output->writeln( "Reading changes from $dir...", OutputInterface::VERBOSITY_DEBUG );
275
		$files   = null; // Make phpcs happy.
276
		$changes = Utils::loadAllChanges( $dir, Config::types(), $this->formatter, $output, $files );
277
		$max     = $files ? max( $files ) : 0;
278
		if ( $max > 0 && ! $this->askToContinue( $input, $output, ( $max > 1 ? 'Errors' : 'Warnings' ) . ' were encountered while reading changes!' ) ) {
279
			return array( self::ASKED_EXIT, null, null );
280
		}
281
		if ( ! $changes && ! $this->askedNoChanges ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $changes of type Automattic\Jetpack\Changelog\ChangeEntry[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
282
			$this->askedNoChanges = true;
283 View Code Duplication
			if ( ! $this->askToContinue( $input, $output, 'No changes were found!' ) ) {
284
				return array( self::NO_CHANGE_EXIT, null, null );
285
			}
286
		}
287
		return array( self::OK_EXIT, $changes, $files );
288
	}
289
290
	/**
291
	 * Deduplicate changes.
292
	 *
293
	 * @param InputInterface  $input InputInterface.
294
	 * @param OutputInterface $output OutputInterface.
295
	 * @param Changelog       $changelog Changelog.
296
	 * @param ChangeEntry[]   $changes Changes.
297
	 * @return int
298
	 */
299
	protected function deduplicateChanges( InputInterface $input, OutputInterface $output, Changelog $changelog, array &$changes ) {
300
		// Deduplicate changes.
301
		if ( ! $changes ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $changes of type Automattic\Jetpack\Changelog\ChangeEntry[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
302
			$output->writeln( 'Skipping deduplication, there are no changes.', OutputInterface::VERBOSITY_DEBUG );
303
			return self::OK_EXIT;
304
		}
305
306
		$depth = (int) $input->getOption( 'deduplicate' );
307
		if ( 0 === $depth ) {
308
			$output->writeln( 'Skipping deduplication, --deduplicate is 0.', OutputInterface::VERBOSITY_DEBUG );
309
			return self::OK_EXIT;
310
		}
311
312
		$output->writeln( "Deduplicating changes from the last $depth version(s)...", OutputInterface::VERBOSITY_DEBUG );
313
		$dedup = array();
314
		foreach ( array_slice( $changelog->getEntries(), 0, $depth ) as $entry ) {
315
			foreach ( $entry->getChanges() as $change ) {
316
				$dedup[ $change->getContent() ] = true;
317
			}
318
		}
319
		unset( $dedup[''] );
320
		if ( $dedup ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dedup of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
321
			$changes = array_filter(
322
				$changes,
323
				function ( $change, $name ) use ( $dedup, $output ) {
324
					if ( isset( $dedup[ $change->getContent() ] ) ) {
325
						$output->writeln( "Found duplicate change in $name.", OutputInterface::VERBOSITY_DEBUG );
326
						return false;
327
					}
328
					return true;
329
				},
330
				ARRAY_FILTER_USE_BOTH
331
			);
332
		}
333 View Code Duplication
		if ( ! $changes && ! $this->askedNoChanges ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $changes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
334
			$this->askedNoChanges = true;
335
			if ( ! $this->askToContinue( $input, $output, 'All changes were duplicates.' ) ) {
336
				return self::NO_CHANGE_EXIT;
337
			}
338
		}
339
		return self::OK_EXIT;
340
	}
341
342
	/**
343
	 * Check whether any changes have content.
344
	 *
345
	 * @param InputInterface  $input InputInterface.
346
	 * @param OutputInterface $output OutputInterface.
347
	 * @param ChangeEntry[]   $changes Changes.
348
	 * @return bool
349
	 */
350
	protected function doChangesHaveContent( InputInterface $input, OutputInterface $output, array $changes ) {
351
		$output->writeln( 'Checking if any changes have content...', OutputInterface::VERBOSITY_DEBUG );
352
		foreach ( $changes as $name => $change ) {
353
			if ( $change->getContent() !== '' ) {
354
				$output->writeln( "Yes, $name has content.", OutputInterface::VERBOSITY_DEBUG );
355
				return true;
356
			}
357
		}
358
		return false;
359
	}
360
361
	/**
362
	 * Apply --amend if applicable.
363
	 *
364
	 * @param InputInterface  $input InputInterface.
365
	 * @param OutputInterface $output OutputInterface.
366
	 * @param Changelog       $changelog Changelog.
367
	 * @param ChangeEntry[]   $changes Changes.
368
	 * @param string|null     $amendedVersion Set to indicate the source version of the amend, if any.
369
	 * @return int
370
	 */
371
	protected function doAmendChanges( InputInterface $input, OutputInterface $output, Changelog $changelog, array &$changes, &$amendedVersion = null ) {
372
		$amendedVersion = null;
373
		if ( $input->getOption( 'amend' ) ) {
374
			$entries = $changelog->getEntries();
375
			if ( $entries ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $entries of type Automattic\Jetpack\Changelog\ChangelogEntry[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
376
				$latest = array_shift( $entries );
377
				$changelog->setEntries( $entries );
378
				$amendedVersion = $latest->getVersion();
379
				$changes        = array_merge( array_values( $changes ), $latest->getEntries() );
0 ignored issues
show
Bug introduced by
The method getEntries() does not seem to exist on object<Automattic\Jetpac...angelog\ChangelogEntry>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
380
				$output->writeln( "Removing changes for $amendedVersion from changelog for --amend.", OutputInterface::VERBOSITY_DEBUG );
381
382
				if ( $input->getOption( 'prologue' ) === null ) {
383
					$input->setOption( 'prologue', $latest->getPrologue() );
384
				}
385
				if ( $input->getOption( 'epilogue' ) === null ) {
386
					$input->setOption( 'epilogue', $latest->getEpilogue() );
387
				}
388
				if ( $input->getOption( 'link' ) === null ) {
389
					$input->setOption( 'link', $latest->getLink() );
390
				}
391
			} else {
392
				$output->writeln( 'No version to amend, ignoring --amend.', OutputInterface::VERBOSITY_DEBUG );
393
			}
394
		}
395
		return self::OK_EXIT;
396
	}
397
398
	/**
399
	 * Sort changes.
400
	 *
401
	 * @param ChangeEntry[] $changes Changes.
402
	 * @return array
403
	 */
404
	protected function sortChanges( array $changes ) {
405
		$sortConfig = array(
406
			'ordering'         => Config::ordering(),
407
			'knownSubheadings' => Config::types(),
408
		);
409
		usort(
410
			$changes,
411
			function ( $a, $b ) use ( $sortConfig, $changes ) {
412
				$ret = ChangeEntry::compare( $a, $b, $sortConfig );
413
				if ( 0 === $ret ) {
414
					// Stability.
415
					$ret = array_search( $a, $changes, true ) - array_search( $b, $changes, true );
416
				}
417
				return $ret;
418
			}
419
		);
420
		return $changes;
421
	}
422
423
	/**
424
	 * Get the version from the command line.
425
	 *
426
	 * @param InputInterface  $input InputInterface.
427
	 * @param OutputInterface $output OutputInterface.
428
	 * @param Changelog       $changelog Changelog.
429
	 * @return string|int New version, or int on error.
430
	 */
431
	protected function getUseVersion( InputInterface $input, OutputInterface $output, Changelog $changelog ) {
432
		$version = $input->getOption( 'use-version' );
433
		$output->writeln( "Using version $version from command line.", OutputInterface::VERBOSITY_DEBUG );
434
435
		// Normalize it?
436
		try {
437
			$nversion = $this->versioning->normalizeVersion( $version );
438
		} catch ( InvalidArgumentException $ex ) {
439
			$nversion = $version;
440
			$this->writeln( "<error>Invalid --use-version: {$ex->getMessage()}{/}" );
441
			if ( ! $this->askToContinue( $input, $output, 'The specified version is not valid. This may cause problems in the future!' ) ) {
442
				return self::ASKED_EXIT;
443
			}
444
		}
445
		if ( $version !== $nversion ) {
446
			if ( ! $input->isInteractive() ) {
447
				if ( ! $this->askToContinue( "The supplied version $version is not normalized, it should be $nversion." ) ) {
448
					return self::ASKED_EXIT;
449
				}
450
			} else {
451
				try {
452
					$question = new ChoiceQuestion(
453
						"The supplied version $version is not normalized",
454
						array(
455
							'proceed'   => "Proceed with $version",
456
							'normalize' => "Normalize to $nversion",
457
							'abort'     => 'Abort',
458
						),
459
						$input->getOption( 'yes' ) ? 'proceed' : 'abort'
460
					);
461
					switch ( $this->getHelper( 'question' )->ask( $input, $output, $question ) ) {
462
						case 'proceed':
463
							break;
464
						case 'normalize':
465
							$output->writeln( "Normalizing $version to $nversion.", OutputInterface::VERBOSITY_DEBUG );
466
							$version = $nversion;
467
							break;
468
						default:
469
							return self::ASKED_EXIT;
470
					}
471
				} catch ( MissingInputException $ex ) { // @codeCoverageIgnore
0 ignored issues
show
Bug introduced by
The class Symfony\Component\Consol...n\MissingInputException does not exist. Did you forget a USE statement, or did you not list all dependencies?

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.

Loading history...
472
					$output->writeln( 'Got EOF when attempting to query user, aborting.', OutputInterface::VERBOSITY_VERBOSE ); // @codeCoverageIgnore
473
					return self::ASKED_EXIT; // @codeCoverageIgnore
474
				}
475
			}
476
		}
477
478
		// Check that it's newer than the current version.
479
		$latest = $changelog->getLatestEntry();
480
		if ( $latest ) {
481
			$curver = $latest->getVersion();
482
			try {
483
				$cmp = $this->versioning->compareVersions( $version, $curver );
484
			} catch ( InvalidArgumentException $ex ) {
485
				$this->writeln( "Cannot compare $version with $curver: {$ex->getMessage()}", OutputInterface::VERBOSITY_DEBUG );
486
				$cmp = 1;
487
			}
488 View Code Duplication
			if ( $cmp < 0 && ! $this->askToContinue( $input, $output, "The most recent version in the changelog is $curver, which comes after $version." ) ) {
489
				return self::ASKED_EXIT;
490
			}
491 View Code Duplication
			if ( 0 === $cmp && ! $this->askToContinue( $input, $output, "The most recent version in the changelog is $curver, which is equivalent to $version." ) ) {
492
				return self::ASKED_EXIT;
493
			}
494
		}
495
		return $version;
496
	}
497
498
	/**
499
	 * Determine the next version.
500
	 *
501
	 * @param InputInterface  $input InputInterface.
502
	 * @param OutputInterface $output OutputInterface.
503
	 * @param Changelog       $changelog Changelog.
504
	 * @param ChangeEntry[]   $changes Changes.
505
	 * @param string|null     $amendedVersion The source version of the amend, if any.
506
	 * @return string|int New version, or int on error.
507
	 */
508
	protected function nextVersion( InputInterface $input, OutputInterface $output, Changelog $changelog, array $changes, $amendedVersion ) {
509
		// Is there a version in the changelog?
510
		$latest = $changelog->getLatestEntry();
511
		if ( ! $latest ) {
512
			if ( null !== $amendedVersion ) {
513
				$output->writeln( "Amending earliest version, reusing version $amendedVersion...", OutputInterface::VERBOSITY_DEBUG );
514
				return $amendedVersion;
515
			}
516
			$output->writeln( '<error>Changelog file contains no entries! Use --use-version to specify the initial version.</>' );
517
			return self::FATAL_EXIT;
518
		}
519
520
		$output->writeln( "Latest version from changelog is {$latest->getVersion()}.", OutputInterface::VERBOSITY_DEBUG );
521
522
		// If they overrode the significance, use that. Otherwise use `$changes`.
523
		if ( $input->getOption( 'use-significance' ) ) {
524
			try {
525
				$verchanges = array(
526
					$this->formatter->newChangeEntry(
527
						array(
528
							'significance' => $input->getOption( 'use-significance' ),
529
							'content'      => 'Dummy',
530
						)
531
					),
532
				);
533
			} catch ( \Exception $ex ) {
534
				$output->writeln( "<error>{$ex->getMessage()}</>" );
535
				return self::FATAL_EXIT;
536
			}
537
		} else {
538
			$verchanges = $changes;
539
		}
540
541
		// Get the next version from the versioning plugin.
542
		$extra = array_filter(
543
			array(
544
				'prerelease' => $input->getOption( 'prerelease' ),
545
				'buildinfo'  => $input->getOption( 'buildinfo' ),
546
			)
547
		);
548
		try {
549
			$version = $this->versioning->nextVersion( $latest->getVersion(), $verchanges, $extra );
550
		} catch ( InvalidArgumentException $ex ) {
551
			// Was it the version from the changelog that made it fail, or something else?
552
			try {
553
				$this->versioning->normalizeVersion( $latest->getVersion() );
554
				$output->writeln( "<error>Failed to determine new version: {$ex->getMessage()}</>" );
555
			} catch ( InvalidArgumentException $ex2 ) {
556
				$output->writeln( "<error>Changelog file contains invalid version {$latest->getVersion()}! Use --use-version to specify the new version.</>" );
557
			}
558
			return self::FATAL_EXIT;
559
		}
560
		$output->writeln( "Next version is {$version}.", OutputInterface::VERBOSITY_DEBUG );
561
562
		// When amending, if the next version turns out to be before the amended version, use the amended version.
563
		try {
564
			if ( null !== $amendedVersion && $this->versioning->compareVersions( $amendedVersion, $version ) > 0 ) {
565
				$output->writeln( "Amended version $amendedVersion is later, using that instead.", OutputInterface::VERBOSITY_DEBUG );
566
				$version = $amendedVersion;
567
			}
568
		} catch ( InvalidArgumentException $ex ) {
569
			$output->writeln( "Amended version $amendedVersion is was not valid. Hope it wasn't supposed to be later.", OutputInterface::VERBOSITY_DEBUG );
570
		}
571
572
		return $version;
573
	}
574
575
	/**
576
	 * Executes the command.
577
	 *
578
	 * @param InputInterface  $input InputInterface.
579
	 * @param OutputInterface $output OutputInterface.
580
	 * @return int 0 If everything went fine, or an exit code.
581
	 */
582
	protected function execute( InputInterface $input, OutputInterface $output ) {
583
		try {
584
			$this->formatter = Config::formatterPlugin();
0 ignored issues
show
Documentation Bug introduced by
It seems like \Automattic\Jetpack\Chan...nfig::formatterPlugin() of type object<Automattic\Jetpack\Changelogger\Formatter> is incompatible with the declared type object<Automattic\Jetpac...logger\FormatterPlugin> of property $formatter.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
585
			$this->formatter->setIO( $input, $output );
586
			$this->versioning = Config::versioningPlugin();
0 ignored issues
show
Documentation Bug introduced by
It seems like \Automattic\Jetpack\Chan...fig::versioningPlugin() of type object<Automattic\Jetpac...hangelogger\Versioning> is incompatible with the declared type object<Automattic\Jetpac...ogger\VersioningPlugin> of property $versioning.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
587
			$this->versioning->setIO( $input, $output );
588
		} catch ( \Exception $ex ) {
589
			$output->writeln( "<error>{$ex->getMessage()}</>" );
590
			return self::FATAL_EXIT;
591
		}
592
		$this->askedNoChanges = false;
593
594
		// Get the changelog.
595
		$changelog = $this->loadChangelog( $input, $output );
596
		if ( is_int( $changelog ) ) {
597
			return $changelog;
598
		}
599
600
		// Get the changes.
601
		list( $ret, $changes, $files ) = $this->loadChanges( $input, $output, $changelog );
602
		if ( self::OK_EXIT !== $ret ) {
603
			return $ret;
604
		}
605
		$ret = $this->deduplicateChanges( $input, $output, $changelog, $changes );
606
		if ( self::OK_EXIT !== $ret ) {
607
			return $ret;
608
		}
609
		$anyChangesWithContent = $this->doChangesHaveContent( $input, $output, $changes );
610 View Code Duplication
		if ( ! $anyChangesWithContent && ! $this->askedNoChanges ) {
611
			$this->askedNoChanges = true;
612
			if ( ! $this->askToContinue( $input, $output, 'There are no changes with content for this write.' ) ) {
613
				return self::NO_CHANGE_EXIT;
614
			}
615
		}
616
		$amendedVersion = null; // Make phpcs happy.
617
		$ret            = $this->doAmendChanges( $input, $output, $changelog, $changes, $amendedVersion );
618
		if ( self::OK_EXIT !== $ret ) {
619
			return $ret;
620
		}
621
		$changes = $this->sortChanges( $changes );
622
623
		// Determine next version.
624
		if ( $input->getOption( 'use-version' ) !== null ) {
625
			$version = $this->getUseVersion( $input, $output, $changelog );
626
		} else {
627
			$version = $this->nextVersion( $input, $output, $changelog, $changes, $amendedVersion );
628
		}
629
		if ( is_int( $version ) ) {
630
			return $version;
631
		}
632
633
		// Add the new changelog entry.
634
		$ret = $this->addEntry( $input, $output, $changelog, $version, $changes );
635
		if ( self::OK_EXIT !== $ret ) {
636
			return $ret;
637
		}
638
639
		// Write the changelog.
640
		$ret = $this->writeChangelog( $input, $output, $changelog );
641
		if ( self::OK_EXIT !== $ret ) {
642
			return $ret;
643
		}
644
645
		// Delete change files and return.
646
		return $this->deleteChanges( $input, $output, $files );
647
	}
648
}
649