Completed
Push — add/testing-info ( be1095...03b7e9 )
by
unknown
09:20
created

WriteCommand   F

Complexity

Total Complexity 96

Size/Duplication

Total Lines 634
Duplicated Lines 4.57 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
dl 29
loc 634
rs 1.966
c 0
b 0
f 0
wmc 96
lcom 1
cbo 7

14 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 42 3
A askToContinue() 0 19 5
A loadChangelog() 5 28 5
A addEntry() 0 23 5
A writeChangelog() 0 20 3
A deleteChanges() 0 19 4
B loadChanges() 6 25 10
B deduplicateChanges() 6 42 10
A doChangesHaveContent() 0 10 3
B doAmendChanges() 0 26 6
A sortChanges() 0 19 2
C getUseVersion() 6 66 16
C nextVersion() 0 70 11
C execute() 6 66 13

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WriteCommand often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WriteCommand, and based on these observations, apply Extract Interface, too.

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