MergeCommand   F
last analyzed

Complexity

Total Complexity 89

Size/Duplication

Total Lines 909
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 34
Bugs 1 Features 0
Metric Value
wmc 89
eloc 389
c 34
b 1
f 0
dl 0
loc 909
ccs 0
cts 435
cp 0
rs 2

25 Methods

Rating   Name   Duplication   Size   Complexity  
A prepareDependencies() 0 10 1
A shouldUseAll() 0 3 1
A printSourceAndTarget() 0 7 1
A getSourceUrl() 0 29 6
A ensureLatestWorkingCopy() 0 33 4
A filterMergeableRevisions() 0 30 6
A completeOptionValues() 0 17 5
B configure() 0 115 1
A getUsableRevisions() 0 26 4
C execute() 0 72 17
A updateWorkingCopy() 0 9 2
A getWorkingCopyUpdateRevision() 0 18 5
A createMergeProgressBar() 0 11 2
A calculateUsableRevisions() 0 30 3
A getMergeCommandArguments() 0 15 3
A getMergedRevisions() 0 14 3
A getDirectRevisions() 0 13 2
A limitRevisions() 0 11 3
B ensureWorkingCopyWithoutConflicts() 0 53 7
A performMerge() 0 60 5
A getAggregatedOptions() 0 3 1
A isReverseMerge() 0 3 1
A performCommit() 0 27 4
A printRevisions() 0 12 1
A getConfigSettings() 0 9 1

How to fix   Complexity   

Complex Class

Complex classes like MergeCommand 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.

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 MergeCommand, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of the SVN-Buddy library.
4
 * For the full copyright and license information, please view
5
 * the LICENSE file that was distributed with this source code.
6
 *
7
 * @copyright Alexander Obuhovich <[email protected]>
8
 * @link      https://github.com/console-helpers/svn-buddy
9
 */
10
11
namespace ConsoleHelpers\SVNBuddy\Command;
12
13
14
use ConsoleHelpers\ConsoleKit\Exception\CommandException;
15
use ConsoleHelpers\SVNBuddy\Config\AbstractConfigSetting;
16
use ConsoleHelpers\SVNBuddy\Config\ArrayConfigSetting;
17
use ConsoleHelpers\SVNBuddy\Config\ChoiceConfigSetting;
18
use ConsoleHelpers\SVNBuddy\Config\StringConfigSetting;
19
use ConsoleHelpers\SVNBuddy\Helper\OutputHelper;
20
use ConsoleHelpers\SVNBuddy\MergeSourceDetector\AbstractMergeSourceDetector;
21
use ConsoleHelpers\SVNBuddy\Repository\Connector\UrlResolver;
22
use ConsoleHelpers\SVNBuddy\Repository\Parser\RevisionListParser;
23
use ConsoleHelpers\SVNBuddy\Repository\WorkingCopyConflictTracker;
24
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
25
use Symfony\Component\Console\Helper\Table;
26
use Symfony\Component\Console\Input\InputArgument;
27
use Symfony\Component\Console\Input\InputInterface;
28
use Symfony\Component\Console\Input\InputOption;
29
use Symfony\Component\Console\Output\OutputInterface;
30
31
class MergeCommand extends AbstractCommand implements IAggregatorAwareCommand, IConfigAwareCommand
32
{
33
34
	const SETTING_MERGE_SOURCE_URL = 'merge.source-url';
35
36
	const SETTING_MERGE_RECENT_CONFLICTS = 'merge.recent-conflicts';
37
38
	const SETTING_MERGE_AUTO_COMMIT = 'merge.auto-commit';
39
40
	const REVISION_ALL = 'all';
41
42
	/**
43
	 * Merge source detector.
44
	 *
45
	 * @var AbstractMergeSourceDetector
46
	 */
47
	private $_mergeSourceDetector;
48
49
	/**
50
	 * Revision list parser.
51
	 *
52
	 * @var RevisionListParser
53
	 */
54
	private $_revisionListParser;
55
56
	/**
57
	 * Usable revisions (either to be merged OR to be unmerged).
58
	 *
59
	 * @var array
60
	 */
61
	private $_usableRevisions = array();
62
63
	/**
64
	 * Url resolver.
65
	 *
66
	 * @var UrlResolver
67
	 */
68
	private $_urlResolver;
69
70
	/**
71
	 * Working copy conflict tracker.
72
	 *
73
	 * @var WorkingCopyConflictTracker
74
	 */
75
	private $_workingCopyConflictTracker;
76
77
	/**
78
	 * Prepare dependencies.
79
	 *
80
	 * @return void
81
	 */
82
	protected function prepareDependencies()
83
	{
84
		parent::prepareDependencies();
85
86
		$container = $this->getContainer();
87
88
		$this->_mergeSourceDetector = $container['merge_source_detector'];
89
		$this->_revisionListParser = $container['revision_list_parser'];
90
		$this->_urlResolver = $container['repository_url_resolver'];
91
		$this->_workingCopyConflictTracker = $container['working_copy_conflict_tracker'];
92
	}
93
94
	/**
95
	 * {@inheritdoc}
96
	 */
97
	protected function configure()
98
	{
99
		$this
100
			->setName('merge')
101
			->setDescription('Merge changes from another project or ref within same project into a working copy')
102
			->addArgument(
103
				'path',
104
				InputArgument::OPTIONAL,
105
				'Working copy path',
106
				'.'
107
			)
108
			->addOption(
109
				'source-url',
110
				null,
111
				InputOption::VALUE_REQUIRED,
112
				'Merge source url (absolute or relative) or ref name, e.g. <comment>branches/branch-name</comment>'
113
			)
114
			->addOption(
115
				'revisions',
116
				'r',
117
				InputOption::VALUE_REQUIRED,
118
				'List of revision(-s) and/or revision range(-s) to merge, e.g. <comment>53324</comment>, <comment>1224-4433</comment> or <comment>all</comment>'
119
			)
120
			->addOption(
121
				'exclude-revisions',
122
				null,
123
				InputOption::VALUE_REQUIRED,
124
				'List of revision(-s) and/or revision range(-s) not to merge, e.g. <comment>53324</comment>, <comment>1224-4433</comment>'
125
			)
126
			->addOption(
127
				'bugs',
128
				'b',
129
				InputOption::VALUE_REQUIRED,
130
				'List of bug(-s) to merge, e.g. <comment>JRA-1234</comment>, <comment>43644</comment>'
131
			)
132
			->addOption(
133
				'exclude-bugs',
134
				null,
135
				InputOption::VALUE_REQUIRED,
136
				'List of bug(-s) not to merge, e.g. <comment>JRA-1234</comment>, <comment>43644</comment>'
137
			)
138
			->addOption(
139
				'merges',
140
				null,
141
				InputOption::VALUE_NONE,
142
				'Show merge revisions only'
143
			)
144
			->addOption(
145
				'no-merges',
146
				null,
147
				InputOption::VALUE_NONE,
148
				'Hide merge revisions'
149
			)
150
			->addOption(
151
				'with-full-message',
152
				'f',
153
				InputOption::VALUE_NONE,
154
				'Shows non-truncated commit messages'
155
			)
156
			->addOption(
157
				'with-details',
158
				'd',
159
				InputOption::VALUE_NONE,
160
				'Shows detailed revision information, e.g. paths affected'
161
			)
162
			->addOption(
163
				'with-summary',
164
				's',
165
				InputOption::VALUE_NONE,
166
				'Shows number of added/changed/removed paths in the revision'
167
			)
168
			->addOption(
169
				'update-revision',
170
				null,
171
				InputOption::VALUE_REQUIRED,
172
				'Update working copy to given revision before performing a merge'
173
			)
174
			->addOption(
175
				'auto-commit',
176
				null,
177
				InputOption::VALUE_REQUIRED,
178
				'Automatically perform commit on successful merge, e.g. <comment>yes</comment> or <comment>no</comment>'
179
			)
180
			->addOption(
181
				'auto-deploy',
182
				null,
183
				InputOption::VALUE_REQUIRED,
184
				'Automatically perform remote deployment on successful merge commit, e.g. <comment>yes</comment> or <comment>no</comment>'
185
			)
186
			->addOption(
187
				'record-only',
188
				null,
189
				InputOption::VALUE_NONE,
190
				'Mark revisions as merged without actually merging them'
191
			)
192
			->addOption(
193
				'reverse',
194
				null,
195
				InputOption::VALUE_NONE,
196
				'Rollback previously merged revisions'
197
			)
198
			->addOption(
199
				'aggregate',
200
				'a',
201
				InputOption::VALUE_NONE,
202
				'Aggregate displayed revisions by bugs'
203
			)
204
			->addOption(
205
				'preview',
206
				'p',
207
				InputOption::VALUE_NONE,
208
				'Preview revisions to be merged'
209
			);
210
211
		parent::configure();
212
	}
213
214
	/**
215
	 * Return possible values for the named option
216
	 *
217
	 * @param string            $optionName Option name.
218
	 * @param CompletionContext $context    Completion context.
219
	 *
220
	 * @return array
221
	 */
222
	public function completeOptionValues($optionName, CompletionContext $context)
223
	{
224
		$ret = parent::completeOptionValues($optionName, $context);
225
226
		if ( $optionName === 'revisions' ) {
227
			return array('all');
228
		}
229
230
		if ( $optionName === 'source-url' ) {
231
			return $this->getAllRefs();
232
		}
233
234
		if ( $optionName === 'auto-commit' || $optionName === 'auto-deploy' ) {
235
			return array('yes', 'no');
236
		}
237
238
		return $ret;
239
	}
240
241
	/**
242
	 * {@inheritdoc}
243
	 *
244
	 * @throws CommandException When everything is merged.
245
	 * @throws CommandException When manually specified revisions are already merged.
246
	 * @throws CommandException When bugs from "--bugs" option are not found.
247
	 */
248
	protected function execute(InputInterface $input, OutputInterface $output)
249
	{
250
		$bugs = $this->getList($this->io->getOption('bugs'));
0 ignored issues
show
Bug introduced by
It seems like $this->io->getOption('bugs') can also be of type string[]; however, parameter $string of ConsoleHelpers\SVNBuddy\...tractCommand::getList() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

250
		$bugs = $this->getList(/** @scrutinizer ignore-type */ $this->io->getOption('bugs'));
Loading history...
251
		$revisions = $this->getList($this->io->getOption('revisions'));
252
253
		$wc_path = $this->getWorkingCopyPath();
254
255
		$this->ensureLatestWorkingCopy($wc_path);
256
257
		$source_url = $this->getSourceUrl($wc_path);
258
		$this->printSourceAndTarget($source_url, $wc_path);
259
		$this->_usableRevisions = $this->getUsableRevisions($source_url, $wc_path);
260
261
		if ( ($bugs || $revisions) && !$this->_usableRevisions ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $revisions 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...
Bug Best Practice introduced by
The expression $bugs 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...
Bug Best Practice introduced by
The expression $this->_usableRevisions 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...
262
			throw new CommandException(\sprintf(
263
				'Nothing to %s.',
264
				$this->isReverseMerge() ? 'reverse-merge' : 'merge'
265
			));
266
		}
267
268
		$this->ensureWorkingCopyWithoutConflicts($source_url, $wc_path);
269
270
		if ( $this->shouldUseAll($revisions) ) {
271
			$revisions = $this->filterMergeableRevisions($this->_usableRevisions, $source_url);
272
		}
273
		else {
274
			if ( $revisions ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $revisions 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...
275
				$revisions = $this->getDirectRevisions($revisions, $source_url);
276
			}
277
278
			if ( $bugs ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $bugs 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...
279
				$revisions_from_bugs = $this->getRevisionLog($source_url)->find('bugs', $bugs);
280
281
				if ( !$revisions_from_bugs ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $revisions_from_bugs 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...
282
					throw new CommandException('Specified bugs aren\'t mentioned in any of revisions');
283
				}
284
285
				$revisions = array_merge($revisions, $revisions_from_bugs);
286
			}
287
288
			$revisions = $this->filterMergeableRevisions($revisions, $source_url);
289
290
			if ( $revisions ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $revisions of type integer[] 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...
291
				$revisions = array_intersect($revisions, $this->_usableRevisions);
292
293
				// Auto-commit instead of failing.
294
				if ( !$revisions && $this->performCommit() ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $revisions 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...
295
					return;
296
				}
297
298
				if ( !$revisions ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $revisions 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...
299
					throw new CommandException(\sprintf(
300
						'Requested revisions are %s',
301
						$this->isReverseMerge() ? 'not yet merged' : 'already merged'
302
					));
303
				}
304
			}
305
		}
306
307
		if ( $revisions ) {
308
			if ( $this->io->getOption('preview') ) {
309
				// Display mergeable revisions according to user-specified filters.
310
				$this->printRevisions($source_url, $revisions);
311
			}
312
			else {
313
				// Perform merge using user-specified filters.
314
				$this->performMerge($source_url, $wc_path, $revisions);
315
			}
316
		}
317
		elseif ( $this->_usableRevisions ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_usableRevisions 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...
318
			// Display all mergeable revisions, because user haven't specified any revisions for merging.
319
			$this->printRevisions($source_url, $this->_usableRevisions);
320
		}
321
	}
322
323
	/**
324
	 * Filters mergeable revision list.
325
	 *
326
	 * @param array  $revisions  Revisions.
327
	 * @param string $source_url Source URL.
328
	 *
329
	 * @return integer[]
330
	 * @throws CommandException When bugs from "--exclude-bugs" option are not found.
331
	 */
332
	protected function filterMergeableRevisions(array $revisions, $source_url)
333
	{
334
		$exclude_bugs = $this->getList($this->io->getOption('exclude-bugs'));
0 ignored issues
show
Bug introduced by
It seems like $this->io->getOption('exclude-bugs') can also be of type string[]; however, parameter $string of ConsoleHelpers\SVNBuddy\...tractCommand::getList() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

334
		$exclude_bugs = $this->getList(/** @scrutinizer ignore-type */ $this->io->getOption('exclude-bugs'));
Loading history...
335
		$exclude_revisions = $this->getList($this->io->getOption('exclude-revisions'));
336
337
		if ( $exclude_revisions ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $exclude_revisions 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...
338
			$revisions = array_diff(
339
				$revisions,
340
				$this->getDirectRevisions($exclude_revisions, $source_url)
341
			);
342
		}
343
344
		if ( $exclude_bugs ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $exclude_bugs 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...
345
			$exclude_revisions_from_bugs = $this->getRevisionLog($source_url)->find('bugs', $exclude_bugs);
346
347
			if ( !$exclude_revisions_from_bugs ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $exclude_revisions_from_bugs 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...
348
				throw new CommandException('Specified exclude-bugs aren\'t mentioned in any of revisions');
349
			}
350
351
			$revisions = array_diff($revisions, $exclude_revisions_from_bugs);
352
		}
353
354
		if ( $this->io->getOption('merges') ) {
355
			$revisions = array_intersect($revisions, $this->getRevisionLog($source_url)->find('merges', 'all_merges'));
356
		}
357
		elseif ( $this->io->getOption('no-merges') ) {
358
			$revisions = array_diff($revisions, $this->getRevisionLog($source_url)->find('merges', 'all_merges'));
359
		}
360
361
		return $revisions;
362
	}
363
364
	/**
365
	 * Determines if all usable revisions should be processed.
366
	 *
367
	 * @param array $revisions Revisions.
368
	 *
369
	 * @return boolean
370
	 */
371
	protected function shouldUseAll(array $revisions)
372
	{
373
		return $revisions === array(self::REVISION_ALL);
374
	}
375
376
	/**
377
	 * Ensures, that working copy is up to date.
378
	 *
379
	 * @param string $wc_path Working copy path.
380
	 *
381
	 * @return void
382
	 */
383
	protected function ensureLatestWorkingCopy($wc_path)
384
	{
385
		$this->io->write(' * Working Copy Status ... ');
386
		$update_revision = $this->io->getOption('update-revision');
387
388
		if ( $this->repositoryConnector->getWorkingCopyMissing($wc_path) ) {
389
			$this->io->writeln('<error>Locally deleted files found</error>');
390
			$this->updateWorkingCopy($wc_path, $update_revision);
391
392
			return;
393
		}
394
395
		$working_copy_revisions = $this->repositoryConnector->getWorkingCopyRevisions($wc_path);
396
397
		if ( count($working_copy_revisions) > 1 ) {
398
			$this->io->writeln(
399
				'<error>Mixed revisions: ' . implode(', ', $working_copy_revisions) . '</error>'
400
			);
401
			$this->updateWorkingCopy($wc_path, $update_revision);
402
403
			return;
404
		}
405
406
		$update_revision = $this->getWorkingCopyUpdateRevision($wc_path);
407
408
		if ( isset($update_revision) ) {
409
			$this->io->writeln('<error>Not at ' . $update_revision . ' revision</error>');
410
			$this->updateWorkingCopy($wc_path, $update_revision);
411
412
			return;
413
		}
414
415
		$this->io->writeln('<info>Up to date</info>');
416
	}
417
418
	/**
419
	 * Returns revision, that working copy needs to be updated to.
420
	 *
421
	 * @param string $wc_path Working copy path.
422
	 *
423
	 * @return integer|null
424
	 */
425
	protected function getWorkingCopyUpdateRevision($wc_path)
426
	{
427
		$update_revision = $this->io->getOption('update-revision');
428
		$actual_revision = $this->repositoryConnector->getLastRevision($wc_path);
429
430
		if ( isset($update_revision) ) {
431
			if ( is_numeric($update_revision) ) {
432
				return (int)$update_revision === (int)$actual_revision ? null : $update_revision;
0 ignored issues
show
Bug Best Practice introduced by
The expression return (int)$update_revi...null : $update_revision also could return the type string which is incompatible with the documented return type integer|null.
Loading history...
433
			}
434
435
			return $update_revision;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $update_revision returns the type boolean|string|string[] which is incompatible with the documented return type integer|null.
Loading history...
436
		}
437
438
		$repository_revision = $this->repositoryConnector->getLastRevision(
439
			$this->repositoryConnector->getWorkingCopyUrl($wc_path)
440
		);
441
442
		return $repository_revision > $actual_revision ? $repository_revision : null;
443
	}
444
445
	/**
446
	 * Updates working copy.
447
	 *
448
	 * @param string     $wc_path  Working copy path.
449
	 * @param mixed|null $revision Revision.
450
	 *
451
	 * @return void
452
	 */
453
	protected function updateWorkingCopy($wc_path, $revision = null)
454
	{
455
		$arguments = array('path' => $wc_path, '--ignore-externals' => true);
456
457
		if ( isset($revision) ) {
458
			$arguments['--revision'] = $revision;
459
		}
460
461
		$this->runOtherCommand('update', $arguments);
462
	}
463
464
	/**
465
	 * Returns source url for merge.
466
	 *
467
	 * @param string $wc_path Working copy path.
468
	 *
469
	 * @return string
470
	 * @throws CommandException When source path is invalid.
471
	 */
472
	protected function getSourceUrl($wc_path)
473
	{
474
		$source_url = $this->io->getOption('source-url');
475
476
		if ( $source_url === null ) {
477
			$source_url = $this->getSetting(self::SETTING_MERGE_SOURCE_URL);
478
		}
479
		elseif ( !$this->repositoryConnector->isUrl($source_url) ) {
0 ignored issues
show
Bug introduced by
It seems like $source_url can also be of type string[]; however, parameter $path of ConsoleHelpers\SVNBuddy\...ctor\Connector::isUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

479
		elseif ( !$this->repositoryConnector->isUrl(/** @scrutinizer ignore-type */ $source_url) ) {
Loading history...
480
			$wc_url = $this->repositoryConnector->getWorkingCopyUrl($wc_path);
481
			$source_url = $this->_urlResolver->resolve($wc_url, $source_url);
0 ignored issues
show
Bug introduced by
It seems like $source_url can also be of type string[]; however, parameter $url_to_resolve of ConsoleHelpers\SVNBuddy\...\UrlResolver::resolve() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

481
			$source_url = $this->_urlResolver->resolve($wc_url, /** @scrutinizer ignore-type */ $source_url);
Loading history...
482
		}
483
484
		if ( !$source_url ) {
485
			$wc_url = $this->repositoryConnector->getWorkingCopyUrl($wc_path);
486
			$source_url = $this->_mergeSourceDetector->detect($wc_url);
487
488
			if ( $source_url ) {
489
				$this->setSetting(self::SETTING_MERGE_SOURCE_URL, $source_url);
490
			}
491
		}
492
493
		if ( !$source_url ) {
494
			$wc_url = $this->repositoryConnector->getWorkingCopyUrl($wc_path);
495
			$error_msg = 'Unable to guess "--source-url" option value. Please specify it manually.' . PHP_EOL;
496
			$error_msg .= 'Working Copy URL: ' . $wc_url . '.';
497
			throw new CommandException($error_msg);
498
		}
499
500
		return $source_url;
501
	}
502
503
	/**
504
	 * Prints information about merge source & target.
505
	 *
506
	 * @param string $source_url Merge source: url.
507
	 * @param string $wc_path    Merge target: working copy path.
508
	 *
509
	 * @return void
510
	 */
511
	protected function printSourceAndTarget($source_url, $wc_path)
512
	{
513
		$relative_source_url = $this->repositoryConnector->getRelativePath($source_url);
514
		$relative_target_url = $this->repositoryConnector->getRelativePath($wc_path);
515
516
		$this->io->writeln(' * Merge Source ... <info>' . $relative_source_url . '</info>');
517
		$this->io->writeln(' * Merge Target ... <info>' . $relative_target_url . '</info>');
518
	}
519
520
	/**
521
	 * Ensures, that there are some usable revisions.
522
	 *
523
	 * @param string $source_url Merge source: url.
524
	 * @param string $wc_path    Merge target: working copy path.
525
	 *
526
	 * @return array
527
	 */
528
	protected function getUsableRevisions($source_url, $wc_path)
529
	{
530
		// Avoid missing revision query progress bar overwriting following output.
531
		$revision_log = $this->getRevisionLog($source_url);
532
533
		$this->io->write(sprintf(
534
			' * Upcoming %s Status (no filters) ... ',
535
			$this->isReverseMerge() ? 'Reverse-merge' : 'Merge'
536
		));
537
		$usable_revisions = $this->calculateUsableRevisions($source_url, $wc_path);
538
539
		if ( $usable_revisions ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $usable_revisions 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...
540
			$usable_bugs = $revision_log->getBugsFromRevisions($usable_revisions);
541
			$error_msg = '<error>%d revision(-s) or %d bug(-s) %s</error>';
542
			$this->io->writeln(sprintf(
543
				$error_msg,
544
				count($usable_revisions),
545
				count($usable_bugs),
546
				$this->isReverseMerge() ? 'merged' : 'not merged'
547
			));
548
		}
549
		else {
550
			$this->io->writeln('<info>Up to date</info>');
551
		}
552
553
		return $usable_revisions;
554
	}
555
556
	/**
557
	 * Returns usable revisions.
558
	 *
559
	 * @param string $source_url Merge source: url.
560
	 * @param string $wc_path    Merge target: working copy path.
561
	 *
562
	 * @return array
563
	 */
564
	protected function calculateUsableRevisions($source_url, $wc_path)
565
	{
566
		$source_url = $this->repositoryConnector->removeCredentials($source_url);
567
568
		$command = $this->repositoryConnector->getCommand(
569
			'mergeinfo',
570
			array(
571
				'--show-revs',
572
				$this->isReverseMerge() ? 'merged' : 'eligible',
573
				$source_url,
574
				$wc_path,
575
			)
576
		);
577
578
		$merge_info = $this->repositoryConnector->getProperty('svn:mergeinfo', $wc_path);
579
580
		$cache_invalidator = array(
581
			'source:' . $this->repositoryConnector->getLastRevision($source_url),
582
			'merged_hash:' . crc32($merge_info),
583
		);
584
		$command->setCacheInvalidator(implode(';', $cache_invalidator));
585
586
		$merge_info = $command->run();
587
		$merge_info = explode(PHP_EOL, $merge_info);
588
589
		foreach ( $merge_info as $index => $revision ) {
590
			$merge_info[$index] = ltrim($revision, 'r');
591
		}
592
593
		return array_filter($merge_info);
594
	}
595
596
	/**
597
	 * Parses information from "svn:mergeinfo" property.
598
	 *
599
	 * @param string $source_path Merge source: path in repository.
600
	 * @param string $wc_path     Merge target: working copy path.
601
	 *
602
	 * @return array
603
	 */
604
	protected function getMergedRevisions($source_path, $wc_path)
605
	{
606
		$merge_info = $this->repositoryConnector->getProperty('svn:mergeinfo', $wc_path);
607
		$merge_info = array_filter(explode("\n", $merge_info));
608
609
		foreach ( $merge_info as $merge_info_line ) {
610
			list($path, $revisions) = explode(':', $merge_info_line, 2);
611
612
			if ( $path === $source_path ) {
613
				return $this->_revisionListParser->expandRanges(explode(',', $revisions));
614
			}
615
		}
616
617
		return array();
618
	}
619
620
	/**
621
	 * Validates revisions to actually exist.
622
	 *
623
	 * @param array  $revisions      Revisions.
624
	 * @param string $repository_url Repository url.
625
	 *
626
	 * @return array
627
	 * @throws CommandException When revision doesn't exist.
628
	 */
629
	protected function getDirectRevisions(array $revisions, $repository_url)
630
	{
631
		$revision_log = $this->getRevisionLog($repository_url);
632
633
		try {
634
			$revisions = $this->_revisionListParser->expandRanges($revisions);
635
			$revision_log->getRevisionsData('summary', $revisions);
636
		}
637
		catch ( \InvalidArgumentException $e ) {
638
			throw new CommandException($e->getMessage());
639
		}
640
641
		return $revisions;
642
	}
643
644
	/**
645
	 * Performs merge.
646
	 *
647
	 * @param string $source_url Merge source: url.
648
	 * @param string $wc_path    Merge target: working copy path.
649
	 * @param array  $revisions  Revisions to merge.
650
	 *
651
	 * @return void
652
	 */
653
	protected function performMerge($source_url, $wc_path, array $revisions)
654
	{
655
		if ( $this->isReverseMerge() ) {
656
			rsort($revisions, SORT_NUMERIC);
657
		}
658
		else {
659
			sort($revisions, SORT_NUMERIC);
660
		}
661
662
		$revision_count = count($revisions);
663
664
		$used_revision_count = 0;
665
		$used_revisions = $this->repositoryConnector->getMergedRevisionChanges($wc_path, !$this->isReverseMerge());
666
667
		if ( $used_revisions ) {
668
			$used_revisions = call_user_func_array('array_merge', $used_revisions);
669
			$used_revision_count = count($used_revisions);
670
			$revision_count += $used_revision_count;
671
		}
672
673
		$revision_log = $this->getRevisionLog($source_url);
674
		$revisions_data = $revision_log->getRevisionsData('summary', $revisions);
675
676
		$revision_title_mask = $revision_log->getRevisionURLBuilder()->getMask('fg=white;options=bold,underscore');
677
678
		// Added " revision" text, when URL wasn't detected.
679
		if ( strpos($revision_title_mask, '://') === false ) {
680
			$revision_title_mask .= ' revision';
681
		}
682
683
		$merge_command_arguments = $this->getMergeCommandArguments($source_url, $wc_path);
684
685
		foreach ( $revisions as $index => $revision ) {
686
			$command_arguments = str_replace('{revision}', $revision, $merge_command_arguments);
687
			$command = $this->repositoryConnector->getCommand('merge', $command_arguments);
688
689
			$progress_bar = $this->createMergeProgressBar($used_revision_count + $index + 1, $revision_count);
690
691
			// 1. Add revision link with a progress bar.
692
			$merge_heading = PHP_EOL . '<fg=white;options=bold>';
693
			$merge_heading .= '--- $1 ' . \str_replace('{revision}', $revision, $revision_title_mask);
694
			$merge_heading .= " into '$2' " . $progress_bar . ':</>';
695
696
			// 2. Add a commit message.
697
			$commit_message = trim($revisions_data[$revision]['msg']);
698
			$commit_message = wordwrap($commit_message, 68); // FIXME: Not UTF-8 safe solution.
699
			$merge_heading .= PHP_EOL . $commit_message;
700
			$merge_heading .= PHP_EOL;
701
			$merge_heading .= PHP_EOL . '<fg=white;options=bold>Changed Paths:</>';
702
703
			$command->runLive(array(
704
				$wc_path => '.',
705
				'/--- (Merging|Reverse-merging) r' . $revision . " into '([^']*)':/" => $merge_heading,
706
			));
707
708
			$this->_usableRevisions = array_diff($this->_usableRevisions, array($revision));
709
			$this->ensureWorkingCopyWithoutConflicts($source_url, $wc_path, $revision);
710
		}
711
712
		$this->performCommit();
713
	}
714
715
	/**
716
	 * Returns merge command arguments.
717
	 *
718
	 * @param string $source_url Merge source: url.
719
	 * @param string $wc_path    Merge target: working copy path.
720
	 *
721
	 * @return array
722
	 */
723
	protected function getMergeCommandArguments($source_url, $wc_path)
724
	{
725
		$ret = array(
726
			'-c',
727
			$this->isReverseMerge() ? '-{revision}' : '{revision}',
728
		);
729
730
		if ( $this->io->getOption('record-only') ) {
731
			$ret[] = '--record-only';
732
		}
733
734
		$ret[] = $source_url;
735
		$ret[] = $wc_path;
736
737
		return $ret;
738
	}
739
740
	/**
741
	 * Create merge progress bar.
742
	 *
743
	 * @param integer $current Current.
744
	 * @param integer $total   Total.
745
	 *
746
	 * @return string
747
	 */
748
	protected function createMergeProgressBar($current, $total)
749
	{
750
		$total_length = 28;
751
		$percent_used = floor(($current / $total) * 100);
752
		$length_used = floor(($total_length * $percent_used) / 100);
753
		$length_free = $total_length - $length_used;
754
755
		$ret = $length_used > 0 ? str_repeat('=', $length_used - 1) : '';
0 ignored issues
show
Bug introduced by
$length_used - 1 of type double is incompatible with the type integer expected by parameter $times of str_repeat(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

755
		$ret = $length_used > 0 ? str_repeat('=', /** @scrutinizer ignore-type */ $length_used - 1) : '';
Loading history...
756
		$ret .= '>' . str_repeat('-', $length_free);
757
758
		return '[' . $ret . '] ' . $percent_used . '% (' . $current . ' of ' . $total . ')';
759
	}
760
761
	/**
762
	 * Ensures, that there are no unresolved conflicts in working copy.
763
	 *
764
	 * @param string  $source_url                 Source url.
765
	 * @param string  $wc_path                    Working copy path.
766
	 * @param integer $largest_suggested_revision Largest revision, that is suggested in error message.
767
	 *
768
	 * @return void
769
	 * @throws CommandException When merge conflicts detected.
770
	 */
771
	protected function ensureWorkingCopyWithoutConflicts($source_url, $wc_path, $largest_suggested_revision = null)
772
	{
773
		$this->io->write(' * Previous Merge Status ... ');
774
775
		$conflicts = $this->_workingCopyConflictTracker->getNewConflicts($wc_path);
776
777
		if ( !$conflicts ) {
778
			$this->io->writeln('<info>Successful</info>');
779
780
			return;
781
		}
782
783
		$this->_workingCopyConflictTracker->add($wc_path);
784
		$this->io->writeln('<error>' . count($conflicts) . ' conflict(-s)</error>');
785
786
		$table = new Table($this->io->getOutput());
787
788
		if ( $largest_suggested_revision ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $largest_suggested_revision of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
789
			$table->setHeaders(array(
790
				'Path',
791
				'Associated Revisions (before ' . $largest_suggested_revision . ')',
792
			));
793
		}
794
		else {
795
			$table->setHeaders(array(
796
				'Path',
797
				'Associated Revisions',
798
			));
799
		}
800
801
		$revision_log = $this->getRevisionLog($source_url);
802
		$source_path = $this->repositoryConnector->getRelativePath($source_url) . '/';
803
804
		/** @var OutputHelper $output_helper */
805
		$output_helper = $this->getHelper('output');
806
807
		foreach ( $conflicts as $conflict_path ) {
808
			$path_revisions = $revision_log->find('paths', $source_path . $conflict_path);
809
			$path_revisions = array_intersect($this->_usableRevisions, $path_revisions);
810
811
			if ( $path_revisions && isset($largest_suggested_revision) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $path_revisions 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...
812
				$path_revisions = $this->limitRevisions($path_revisions, $largest_suggested_revision);
813
			}
814
815
			$table->addRow(array(
816
				$conflict_path,
817
				$path_revisions ? $output_helper->formatArray($path_revisions, 4) : '-',
818
			));
819
		}
820
821
		$table->render();
822
823
		throw new CommandException('Working copy contains unresolved merge conflicts.');
824
	}
825
826
	/**
827
	 * Returns revisions not larger, then given one.
828
	 *
829
	 * @param array   $revisions    Revisions.
830
	 * @param integer $max_revision Maximal revision.
831
	 *
832
	 * @return array
833
	 */
834
	protected function limitRevisions(array $revisions, $max_revision)
835
	{
836
		$ret = array();
837
838
		foreach ( $revisions as $revision ) {
839
			if ( $revision < $max_revision ) {
840
				$ret[] = $revision;
841
			}
842
		}
843
844
		return $ret;
845
	}
846
847
	/**
848
	 * Performs commit unless user doesn't want it.
849
	 *
850
	 * @return boolean
851
	 */
852
	protected function performCommit()
853
	{
854
		$auto_commit = $this->io->getOption('auto-commit');
855
856
		if ( $auto_commit !== null ) {
857
			$auto_commit = $auto_commit === 'yes';
858
		}
859
		else {
860
			$auto_commit = (boolean)$this->getSetting(self::SETTING_MERGE_AUTO_COMMIT);
861
		}
862
863
		if ( $auto_commit ) {
864
			$auto_deploy = $this->io->getOption('auto-deploy');
865
866
			$commit_arguments = array(
867
				'path' => $this->io->getArgument('path'),
868
			);
869
870
			if ( $auto_deploy !== null ) {
871
				$commit_arguments['--auto-deploy'] = $auto_deploy;
872
			}
873
874
			$this->io->writeln(array('', 'Commencing automatic commit after merge ...'));
875
			$this->runOtherCommand('commit', $commit_arguments);
876
		}
877
878
		return $auto_commit;
879
	}
880
881
	/**
882
	 * Prints revisions.
883
	 *
884
	 * @param string $source_url Merge source: url.
885
	 * @param array  $revisions  Revisions.
886
	 *
887
	 * @return void
888
	 */
889
	protected function printRevisions($source_url, array $revisions)
890
	{
891
		$this->runOtherCommand('log', array(
892
			'path' => $source_url,
893
			'--revisions' => implode(',', $revisions),
894
			'--merges' => $this->io->getOption('merges'),
895
			'--no-merges' => $this->io->getOption('no-merges'),
896
			'--with-full-message' => $this->io->getOption('with-full-message'),
897
			'--with-details' => $this->io->getOption('with-details'),
898
			'--with-summary' => $this->io->getOption('with-summary'),
899
			'--aggregate' => $this->io->getOption('aggregate'),
900
			'--with-merge-oracle' => true,
901
		));
902
	}
903
904
	/**
905
	 * Returns list of config settings.
906
	 *
907
	 * @return AbstractConfigSetting[]
908
	 */
909
	public function getConfigSettings()
910
	{
911
		return array(
912
			new StringConfigSetting(self::SETTING_MERGE_SOURCE_URL, ''),
913
			new ArrayConfigSetting(self::SETTING_MERGE_RECENT_CONFLICTS, array()),
914
			new ChoiceConfigSetting(
915
				self::SETTING_MERGE_AUTO_COMMIT,
916
				array(1 => 'Yes', 0 => 'No'),
917
				1
918
			),
919
		);
920
	}
921
922
	/**
923
	 * Returns option names, that makes sense to use in aggregation mode.
924
	 *
925
	 * @return array
926
	 */
927
	public function getAggregatedOptions()
928
	{
929
		return array('with-full-message', 'with-details', 'with-summary');
930
	}
931
932
	/**
933
	 * Determines if merge should be done in opposite direction (unmerge).
934
	 *
935
	 * @return boolean
936
	 */
937
	protected function isReverseMerge()
938
	{
939
		return $this->io->getOption('reverse');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->io->getOption('reverse') also could return the type string|string[] which is incompatible with the documented return type boolean.
Loading history...
940
	}
941
942
}
943