Completed
Push — add/jetpack-license-ui ( af6bbd...2c376d )
by
unknown
395:57 queued 385:29
created

WriteCommand   F

Complexity

Total Complexity 95

Size/Duplication

Total Lines 629
Duplicated Lines 4.61 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
dl 29
loc 629
rs 1.971
c 0
b 0
f 0
wmc 95
lcom 1
cbo 7

14 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 41 3
A askToContinue() 0 19 5
A loadChangelog() 5 28 5
A addEntry() 0 19 4
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( '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
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...
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 ) {
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...
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 ) {
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...
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 ) {
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...
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 ) {
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...
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 ) {
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...
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
		$changes    = array_values( $changes );
412
		usort(
413
			$changes,
414
			function ( $a, $b ) use ( $sortConfig, $changes ) {
415
				$ret = ChangeEntry::compare( $a, $b, $sortConfig );
416
				if ( 0 === $ret ) {
417
					// Stability.
418
					$ret = array_search( $a, $changes, true ) - array_search( $b, $changes, true );
419
				}
420
				return $ret;
421
			}
422
		);
423
		return $changes;
424
	}
425
426
	/**
427
	 * Get the version from the command line.
428
	 *
429
	 * @param InputInterface  $input InputInterface.
430
	 * @param OutputInterface $output OutputInterface.
431
	 * @param Changelog       $changelog Changelog.
432
	 * @return string|int New version, or int on error.
433
	 */
434
	protected function getUseVersion( InputInterface $input, OutputInterface $output, Changelog $changelog ) {
435
		$version = $input->getOption( 'use-version' );
436
		$output->writeln( "Using version $version from command line.", OutputInterface::VERBOSITY_DEBUG );
437
438
		// Normalize it?
439
		try {
440
			$nversion = $this->versioning->normalizeVersion( $version );
441
		} catch ( InvalidArgumentException $ex ) {
442
			$nversion = $version;
443
			$output->writeln( "<error>Invalid --use-version: {$ex->getMessage()}</>" );
444
			if ( ! $this->askToContinue( $input, $output, 'The specified version is not valid. This may cause problems in the future!' ) ) {
445
				return self::ASKED_EXIT;
446
			}
447
		}
448
		if ( $version !== $nversion ) {
449
			if ( ! $input->isInteractive() ) {
450
				if ( ! $this->askToContinue( $input, $output, "The supplied version $version is not normalized, it should be $nversion." ) ) {
451
					return self::ASKED_EXIT;
452
				}
453
			} else {
454
				try {
455
					$question = new ChoiceQuestion(
456
						"The supplied version $version is not normalized.",
457
						array(
458
							'proceed'   => "Proceed with $version",
459
							'normalize' => "Normalize to $nversion",
460
							'abort'     => 'Abort',
461
						),
462
						$input->getOption( 'yes' ) ? 'proceed' : 'abort'
463
					);
464
					switch ( $this->getHelper( 'question' )->ask( $input, $output, $question ) ) {
465
						case 'proceed': // @codeCoverageIgnore
466
							break;
467
						case 'normalize': // @codeCoverageIgnore
468
							$output->writeln( "Normalizing $version to $nversion.", OutputInterface::VERBOSITY_DEBUG );
469
							$version = $nversion;
470
							break;
471
						default:
472
							return self::ASKED_EXIT;
473
					}
474
				} 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...
475
					$output->writeln( 'Got EOF when attempting to query user, aborting.', OutputInterface::VERBOSITY_VERBOSE ); // @codeCoverageIgnore
476
					return self::ASKED_EXIT; // @codeCoverageIgnore
477
				}
478
			}
479
		}
480
481
		// Check that it's newer than the current version.
482
		$latest = $changelog->getLatestEntry();
483
		if ( $latest ) {
484
			$curver = $latest->getVersion();
485
			try {
486
				$cmp = $this->versioning->compareVersions( $version, $curver );
487
			} catch ( InvalidArgumentException $ex ) {
488
				$output->writeln( "Cannot compare $version with $curver: {$ex->getMessage()}", OutputInterface::VERBOSITY_DEBUG );
489
				$cmp = 1;
490
			}
491 View Code Duplication
			if ( $cmp < 0 && ! $this->askToContinue( $input, $output, "The most recent version in the changelog is $curver, which comes after $version." ) ) {
492
				return self::ASKED_EXIT;
493
			}
494 View Code Duplication
			if ( 0 === $cmp && ! $this->askToContinue( $input, $output, "The most recent version in the changelog is $curver, which is equivalent to $version." ) ) {
495
				return self::ASKED_EXIT;
496
			}
497
		}
498
		return $version;
499
	}
500
501
	/**
502
	 * Determine the next version.
503
	 *
504
	 * @param InputInterface  $input InputInterface.
505
	 * @param OutputInterface $output OutputInterface.
506
	 * @param Changelog       $changelog Changelog.
507
	 * @param ChangeEntry[]   $changes Changes.
508
	 * @param string|null     $amendedVersion The source version of the amend, if any.
509
	 * @return string|int New version, or int on error.
510
	 */
511
	protected function nextVersion( InputInterface $input, OutputInterface $output, Changelog $changelog, array $changes, $amendedVersion ) {
512
		$extra = array_filter(
513
			array(
514
				'prerelease' => $input->getOption( 'prerelease' ),
515
				'buildinfo'  => $input->getOption( 'buildinfo' ),
516
			)
517
		);
518
519
		// Is there a version in the changelog?
520
		$latest = $changelog->getLatestEntry();
521
		if ( ! $latest ) {
522
			if ( null !== $amendedVersion ) {
523
				$output->writeln( "Amending earliest version, reusing version $amendedVersion...", OutputInterface::VERBOSITY_DEBUG );
524
				return $amendedVersion;
525
			} elseif ( $input->getOption( 'default-first-version' ) ) {
526
				return $this->versioning->firstVersion( $extra );
527
			} else {
528
				$output->writeln( '<error>Changelog file contains no entries! Use --use-version to specify the initial version.</>' );
529
				return self::FATAL_EXIT;
530
			}
531
		}
532
533
		$output->writeln( "Latest version from changelog is {$latest->getVersion()}.", OutputInterface::VERBOSITY_DEBUG );
534
535
		// If they overrode the significance, use that. Otherwise use `$changes`.
536
		if ( $input->getOption( 'use-significance' ) ) {
537
			try {
538
				$verchanges = array(
539
					$this->formatter->newChangeEntry(
540
						array(
541
							'significance' => $input->getOption( 'use-significance' ),
542
							'content'      => 'Dummy',
543
						)
544
					),
545
				);
546
			} catch ( \Exception $ex ) {
547
				$output->writeln( "<error>{$ex->getMessage()}</>" );
548
				return self::FATAL_EXIT;
549
			}
550
		} else {
551
			$verchanges = $changes;
552
		}
553
554
		// Get the next version from the versioning plugin.
555
		try {
556
			$version = $this->versioning->nextVersion( $latest->getVersion(), $verchanges, $extra );
557
		} catch ( InvalidArgumentException $ex ) {
558
			// Was it the version from the changelog that made it fail, or something else?
559
			try {
560
				$this->versioning->normalizeVersion( $latest->getVersion() );
561
				$output->writeln( "<error>Failed to determine new version: {$ex->getMessage()}</>" );
562
			} catch ( InvalidArgumentException $ex2 ) {
563
				$output->writeln( "<error>Changelog file contains invalid version {$latest->getVersion()}! Use --use-version to specify the new version.</>" );
564
			}
565
			return self::FATAL_EXIT;
566
		}
567
		$output->writeln( "Next version is {$version}.", OutputInterface::VERBOSITY_DEBUG );
568
569
		// When amending, if the next version turns out to be before the amended version, use the amended version.
570
		try {
571
			if ( null !== $amendedVersion && $this->versioning->compareVersions( $amendedVersion, $version ) > 0 ) {
572
				$output->writeln( "Amended version $amendedVersion is later, using that instead.", OutputInterface::VERBOSITY_DEBUG );
573
				$version = $amendedVersion;
574
			}
575
		} catch ( InvalidArgumentException $ex ) {
576
			$output->writeln( "Amended version $amendedVersion is was not valid. Hope it wasn't supposed to be later.", OutputInterface::VERBOSITY_DEBUG );
577
		}
578
579
		return $version;
580
	}
581
582
	/**
583
	 * Executes the command.
584
	 *
585
	 * @param InputInterface  $input InputInterface.
586
	 * @param OutputInterface $output OutputInterface.
587
	 * @return int 0 If everything went fine, or an exit code.
588
	 */
589
	protected function execute( InputInterface $input, OutputInterface $output ) {
590
		try {
591
			$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...
592
			$this->formatter->setIO( $input, $output );
593
			$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...
594
			$this->versioning->setIO( $input, $output );
595
		} catch ( \Exception $ex ) {
596
			$output->writeln( "<error>{$ex->getMessage()}</>" );
597
			return self::FATAL_EXIT;
598
		}
599
		$this->askedNoChanges = false;
600
601
		// Get the changelog.
602
		$changelog = $this->loadChangelog( $input, $output );
603
		if ( is_int( $changelog ) ) {
604
			return $changelog;
605
		}
606
607
		// Get the changes.
608
		list( $ret, $changes, $files ) = $this->loadChanges( $input, $output, $changelog );
609
		if ( self::OK_EXIT !== $ret ) {
610
			return $ret;
611
		}
612
		$ret = $this->deduplicateChanges( $input, $output, $changelog, $changes );
613
		if ( self::OK_EXIT !== $ret ) {
614
			return $ret;
615
		}
616
		$anyChangesWithContent = $this->doChangesHaveContent( $input, $output, $changes );
617 View Code Duplication
		if ( ! $anyChangesWithContent && ! $this->askedNoChanges ) {
618
			$this->askedNoChanges = true;
619
			if ( ! $this->askToContinue( $input, $output, 'There are no changes with content for this write.' ) ) {
620
				return self::NO_CHANGE_EXIT;
621
			}
622
		}
623
		$amendedVersion = null; // Make phpcs happy.
624
		$ret            = $this->doAmendChanges( $input, $output, $changelog, $changes, $amendedVersion );
625
		if ( self::OK_EXIT !== $ret ) {
626
			return $ret; // @codeCoverageIgnore
627
		}
628
		$changes = $this->sortChanges( $changes );
629
630
		// Determine next version.
631
		if ( $input->getOption( 'use-version' ) !== null ) {
632
			$version = $this->getUseVersion( $input, $output, $changelog );
633
		} else {
634
			$version = $this->nextVersion( $input, $output, $changelog, $changes, $amendedVersion );
635
		}
636
		if ( is_int( $version ) ) {
637
			return $version;
638
		}
639
640
		// Add the new changelog entry.
641
		$ret = $this->addEntry( $input, $output, $changelog, $version, $changes );
642
		if ( self::OK_EXIT !== $ret ) {
643
			return $ret;
644
		}
645
646
		// Write the changelog.
647
		$ret = $this->writeChangelog( $input, $output, $changelog );
648
		if ( self::OK_EXIT !== $ret ) {
649
			return $ret;
650
		}
651
652
		// Delete change files and return.
653
		return $this->deleteChanges( $input, $output, $files );
654
	}
655
}
656