Failed Conditions
Push — master ( c1b2a8...af40d0 )
by Alexander
04:07
created

MergeCommand::performMerge()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 0
cts 17
cp 0
rs 9.2
c 0
b 0
f 0
cc 2
eloc 13
nc 2
nop 3
crap 6
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\SVNBuddy\Config\AbstractConfigSetting;
15
use ConsoleHelpers\SVNBuddy\Config\ArrayConfigSetting;
16
use ConsoleHelpers\SVNBuddy\Config\ChoiceConfigSetting;
17
use ConsoleHelpers\SVNBuddy\Config\StringConfigSetting;
18
use ConsoleHelpers\ConsoleKit\Exception\CommandException;
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
	 * Unmerged revisions.
58
	 *
59
	 * @var array
60
	 */
61
	private $_unmergedRevisions = 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
				'bugs',
122
				'b',
123
				InputOption::VALUE_REQUIRED,
124
				'List of bug(-s) to merge, e.g. <comment>JRA-1234</comment>, <comment>43644</comment>'
125
			)
126
			->addOption(
127
				'with-details',
128
				'd',
129
				InputOption::VALUE_NONE,
130
				'Shows detailed revision information, e.g. paths affected'
131
			)
132
			->addOption(
133
				'with-summary',
134
				's',
135
				InputOption::VALUE_NONE,
136
				'Shows number of added/changed/removed paths in the revision'
137
			)
138
			->addOption(
139
				'update-revision',
140
				null,
141
				InputOption::VALUE_REQUIRED,
142
				'Update working copy to given revision before performing a merge'
143
			)
144
			->addOption(
145
				'auto-commit',
146
				null,
147
				InputOption::VALUE_REQUIRED,
148
				'Automatically perform commit on successful merge, e.g. <comment>yes</comment> or <comment>no</comment>'
149
			);
150
151
		parent::configure();
152
	}
153
154
	/**
155
	 * Return possible values for the named option
156
	 *
157
	 * @param string            $optionName Option name.
158
	 * @param CompletionContext $context    Completion context.
159
	 *
160
	 * @return array
161
	 */
162
	public function completeOptionValues($optionName, CompletionContext $context)
163
	{
164
		$ret = parent::completeOptionValues($optionName, $context);
165
166
		if ( $optionName === 'revisions' ) {
167
			return array('all');
168
		}
169
170
		if ( $optionName === 'source-url' ) {
171
			return $this->getAllRefs();
172
		}
173
174
		if ( $optionName === 'auto-commit' ) {
175
			return array('yes', 'no');
176
		}
177
178
		return $ret;
179
	}
180
181
	/**
182
	 * {@inheritdoc}
183
	 *
184
	 * @throws \RuntimeException When both "--bugs" and "--revisions" options were specified.
185
	 * @throws CommandException When everything is merged.
186
	 * @throws CommandException When manually specified revisions are already merged.
187
	 */
188
	protected function execute(InputInterface $input, OutputInterface $output)
189
	{
190
		$bugs = $this->getList($this->io->getOption('bugs'));
191
		$revisions = $this->getList($this->io->getOption('revisions'));
192
193
		if ( $bugs && $revisions ) {
2 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...
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...
194
			throw new \RuntimeException('The "--bugs" and "--revisions" options are mutually exclusive.');
195
		}
196
197
		$wc_path = $this->getWorkingCopyPath();
198
199
		$this->ensureLatestWorkingCopy($wc_path);
200
201
		$source_url = $this->getSourceUrl($wc_path);
202
		$this->printSourceAndTarget($source_url, $wc_path);
203
		$this->_unmergedRevisions = $this->getUnmergedRevisions($source_url, $wc_path);
204
205
		if ( ($bugs || $revisions) && !$this->_unmergedRevisions ) {
3 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...
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 $this->_unmergedRevisions 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...
206
			throw new CommandException('Nothing to merge.');
207
		}
208
209
		$this->ensureWorkingCopyWithoutConflicts($source_url, $wc_path);
210
211
		if ( $this->shouldMergeAll($revisions) ) {
212
			$revisions = $this->_unmergedRevisions;
213
		}
214
		else {
215
			if ( $revisions ) {
1 ignored issue
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...
216
				$revisions = $this->getDirectRevisions($revisions, $source_url);
217
			}
218
			elseif ( $bugs ) {
1 ignored issue
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...
219
				$revisions = $this->getRevisionLog($source_url)->find('bugs', $bugs);
220
			}
221
222
			if ( $revisions ) {
1 ignored issue
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...
223
				$revisions = array_intersect($revisions, $this->_unmergedRevisions);
224
225
				if ( !$revisions ) {
1 ignored issue
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...
226
					throw new CommandException('Requested revisions are already merged');
227
				}
228
			}
229
		}
230
231
		if ( $revisions ) {
232
			$this->performMerge($source_url, $wc_path, $revisions);
233
		}
234
		elseif ( $this->_unmergedRevisions ) {
1 ignored issue
show
Bug Best Practice introduced by
The expression $this->_unmergedRevisions 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...
235
			$this->runOtherCommand('log', array(
236
				'path' => $source_url,
237
				'--revisions' => implode(',', $this->_unmergedRevisions),
238
				'--with-details' => $this->io->getOption('with-details'),
239
				'--with-summary' => $this->io->getOption('with-summary'),
240
				'--with-merge-oracle' => true,
241
			));
242
		}
243
	}
244
245
	/**
246
	 * Determines if all unmerged revisions should be merged.
247
	 *
248
	 * @param array $revisions Revisions.
249
	 *
250
	 * @return boolean
251
	 */
252
	protected function shouldMergeAll(array $revisions)
253
	{
254
		return $revisions === array(self::REVISION_ALL);
255
	}
256
257
	/**
258
	 * Ensures, that working copy is up to date.
259
	 *
260
	 * @param string $wc_path Working copy path.
261
	 *
262
	 * @return void
263
	 */
264
	protected function ensureLatestWorkingCopy($wc_path)
265
	{
266
		$this->io->write(' * Working Copy Status ... ');
267
		$update_revision = $this->io->getOption('update-revision');
268
269
		if ( $this->repositoryConnector->getWorkingCopyMissing($wc_path) ) {
270
			$this->io->writeln('<error>Locally deleted files found</error>');
271
			$this->updateWorkingCopy($wc_path, $update_revision);
272
273
			return;
274
		}
275
276
		if ( $this->repositoryConnector->isMixedRevisionWorkingCopy($wc_path) ) {
277
			$this->io->writeln('<error>Mixed revisions</error>');
278
			$this->updateWorkingCopy($wc_path, $update_revision);
279
280
			return;
281
		}
282
283
		$update_revision = $this->getWorkingCopyUpdateRevision($wc_path);
284
285
		if ( isset($update_revision) ) {
286
			$this->io->writeln('<error>Not at ' . $update_revision . ' revision</error>');
287
			$this->updateWorkingCopy($wc_path, $update_revision);
288
289
			return;
290
		}
291
292
		$this->io->writeln('<info>Up to date</info>');
293
	}
294
295
	/**
296
	 * Returns revision, that working copy needs to be updated to.
297
	 *
298
	 * @param string $wc_path Working copy path.
299
	 *
300
	 * @return integer|null
301
	 */
302
	protected function getWorkingCopyUpdateRevision($wc_path)
303
	{
304
		$update_revision = $this->io->getOption('update-revision');
305
		$actual_revision = $this->repositoryConnector->getLastRevision($wc_path);
306
307
		if ( isset($update_revision) ) {
308
			if ( is_numeric($update_revision) ) {
309
				return (int)$update_revision === (int)$actual_revision ? null : $update_revision;
310
			}
311
312
			return $update_revision;
313
		}
314
315
		$repository_revision = $this->repositoryConnector->getLastRevision(
316
			$this->repositoryConnector->getWorkingCopyUrl($wc_path)
317
		);
318
319
		return $repository_revision > $actual_revision ? $repository_revision : null;
320
	}
321
322
	/**
323
	 * Updates working copy.
324
	 *
325
	 * @param string     $wc_path  Working copy path.
326
	 * @param mixed|null $revision Revision.
327
	 *
328
	 * @return void
329
	 */
330
	protected function updateWorkingCopy($wc_path, $revision = null)
331
	{
332
		$arguments = array('path' => $wc_path, '--ignore-externals' => true);
333
334
		if ( isset($revision) ) {
335
			$arguments['--revision'] = $revision;
336
		}
337
338
		$this->runOtherCommand('update', $arguments);
339
	}
340
341
	/**
342
	 * Returns source url for merge.
343
	 *
344
	 * @param string $wc_path Working copy path.
345
	 *
346
	 * @return string
347
	 * @throws CommandException When source path is invalid.
348
	 */
349
	protected function getSourceUrl($wc_path)
350
	{
351
		$source_url = $this->io->getOption('source-url');
352
353
		if ( $source_url === null ) {
354
			$source_url = $this->getSetting(self::SETTING_MERGE_SOURCE_URL);
355
		}
356
		elseif ( !$this->repositoryConnector->isUrl($source_url) ) {
357
			$wc_url = $this->repositoryConnector->getWorkingCopyUrl($wc_path);
358
			$source_url = $this->_urlResolver->resolve($wc_url, $source_url);
359
		}
360
361
		if ( !$source_url ) {
362
			$wc_url = $this->repositoryConnector->getWorkingCopyUrl($wc_path);
363
			$source_url = $this->_mergeSourceDetector->detect($wc_url);
364
365
			if ( $source_url ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $source_url of type null|string is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
366
				$this->setSetting(self::SETTING_MERGE_SOURCE_URL, $source_url);
367
			}
368
		}
369
370
		if ( !$source_url ) {
371
			$wc_url = $this->repositoryConnector->getWorkingCopyUrl($wc_path);
372
			$error_msg = 'Unable to guess "--source-url" option value. Please specify it manually.' . PHP_EOL;
373
			$error_msg .= 'Working Copy URL: ' . $wc_url . '.';
374
			throw new CommandException($error_msg);
375
		}
376
377
		return $source_url;
378
	}
379
380
	/**
381
	 * Prints information about merge source & target.
382
	 *
383
	 * @param string $source_url Merge source: url.
384
	 * @param string $wc_path    Merge target: working copy path.
385
	 *
386
	 * @return void
387
	 */
388
	protected function printSourceAndTarget($source_url, $wc_path)
389
	{
390
		$relative_source_url = $this->repositoryConnector->getRelativePath($source_url);
391
		$relative_target_url = $this->repositoryConnector->getRelativePath($wc_path);
392
393
		$this->io->writeln(' * Merge Source ... <info>' . $relative_source_url . '</info>');
394
		$this->io->writeln(' * Merge Target ... <info>' . $relative_target_url . '</info>');
395
	}
396
397
	/**
398
	 * Ensures, that there are some unmerged revisions.
399
	 *
400
	 * @param string $source_url Merge source: url.
401
	 * @param string $wc_path    Merge target: working copy path.
402
	 *
403
	 * @return array
404
	 */
405
	protected function getUnmergedRevisions($source_url, $wc_path)
406
	{
407
		// Avoid missing revision query progress bar overwriting following output.
408
		$revision_log = $this->getRevisionLog($source_url);
409
410
		$this->io->write(' * Upcoming Merge Status ... ');
411
		$unmerged_revisions = $this->calculateUnmergedRevisions($source_url, $wc_path);
412
413
		if ( $unmerged_revisions ) {
1 ignored issue
show
Bug Best Practice introduced by
The expression $unmerged_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...
414
			$unmerged_bugs = $revision_log->getBugsFromRevisions($unmerged_revisions);
415
			$error_msg = '<error>%d revision(-s) or %d bug(-s) not merged</error>';
416
			$this->io->writeln(sprintf($error_msg, count($unmerged_revisions), count($unmerged_bugs)));
417
		}
418
		else {
419
			$this->io->writeln('<info>Up to date</info>');
420
		}
421
422
		return $unmerged_revisions;
423
	}
424
425
	/**
426
	 * Returns not merged revisions.
427
	 *
428
	 * @param string $source_url Merge source: url.
429
	 * @param string $wc_path    Merge target: working copy path.
430
	 *
431
	 * @return array
432
	 */
433
	protected function calculateUnmergedRevisions($source_url, $wc_path)
434
	{
435
		$command = $this->repositoryConnector->getCommand(
436
			'mergeinfo',
437
			'--show-revs eligible {' . $source_url . '} {' . $wc_path . '}'
438
		);
439
440
		$merge_info = $this->repositoryConnector->getProperty('svn:mergeinfo', $wc_path);
441
442
		$cache_invalidator = array(
443
			'source:' . $this->repositoryConnector->getLastRevision($source_url),
444
			'merged_hash:' . crc32($merge_info),
445
		);
446
		$command->setCacheInvalidator(implode(';', $cache_invalidator));
447
448
		$merge_info = $command->run();
449
		$merge_info = explode(PHP_EOL, $merge_info);
450
451
		foreach ( $merge_info as $index => $revision ) {
452
			$merge_info[$index] = ltrim($revision, 'r');
453
		}
454
455
		return array_filter($merge_info);
456
	}
457
458
	/**
459
	 * Parses information from "svn:mergeinfo" property.
460
	 *
461
	 * @param string $source_path Merge source: path in repository.
462
	 * @param string $wc_path     Merge target: working copy path.
463
	 *
464
	 * @return array
465
	 */
466
	protected function getMergedRevisions($source_path, $wc_path)
467
	{
468
		$merge_info = $this->repositoryConnector->getProperty('svn:mergeinfo', $wc_path);
469
		$merge_info = array_filter(explode("\n", $merge_info));
470
471
		foreach ( $merge_info as $merge_info_line ) {
472
			list($path, $revisions) = explode(':', $merge_info_line, 2);
473
474
			if ( $path === $source_path ) {
475
				return $this->_revisionListParser->expandRanges(explode(',', $revisions));
476
			}
477
		}
478
479
		return array();
480
	}
481
482
	/**
483
	 * Validates revisions to actually exist.
484
	 *
485
	 * @param array  $revisions      Revisions.
486
	 * @param string $repository_url Repository url.
487
	 *
488
	 * @return array
489
	 * @throws CommandException When revision doesn't exist.
490
	 */
491
	protected function getDirectRevisions(array $revisions, $repository_url)
492
	{
493
		$revision_log = $this->getRevisionLog($repository_url);
494
495
		try {
496
			$revisions = $this->_revisionListParser->expandRanges($revisions);
497
			$revision_log->getRevisionsData('summary', $revisions);
498
		}
499
		catch ( \InvalidArgumentException $e ) {
500
			throw new CommandException($e->getMessage());
501
		}
502
503
		return $revisions;
504
	}
505
506
	/**
507
	 * Performs merge.
508
	 *
509
	 * @param string $source_url Merge source: url.
510
	 * @param string $wc_path    Merge target: working copy path.
511
	 * @param array  $revisions  Revisions to merge.
512
	 *
513
	 * @return void
514
	 */
515
	protected function performMerge($source_url, $wc_path, array $revisions)
516
	{
517
		sort($revisions, SORT_NUMERIC);
518
519
		foreach ( $revisions as $revision ) {
520
			$command = $this->repositoryConnector->getCommand(
521
				'merge',
522
				'-c ' . $revision . ' {' . $source_url . '} {' . $wc_path . '}'
523
			);
524
525
			$merge_line = '--- Merging r' . $revision . " into '.':";
526
			$command->runLive(array(
527
				$wc_path => '.',
528
				$merge_line => '<fg=white;options=bold>' . $merge_line . '</>',
529
			));
530
531
			$this->_unmergedRevisions = array_diff($this->_unmergedRevisions, array($revision));
532
			$this->ensureWorkingCopyWithoutConflicts($source_url, $wc_path, $revision);
533
		}
534
535
		$this->performCommit();
536
	}
537
538
	/**
539
	 * Ensures, that there are no unresolved conflicts in working copy.
540
	 *
541
	 * @param string  $source_url                 Source url.
542
	 * @param string  $wc_path                    Working copy path.
543
	 * @param integer $largest_suggested_revision Largest revision, that is suggested in error message.
544
	 *
545
	 * @return void
546
	 * @throws CommandException When merge conflicts detected.
547
	 */
548
	protected function ensureWorkingCopyWithoutConflicts($source_url, $wc_path, $largest_suggested_revision = null)
549
	{
550
		$this->io->write(' * Previous Merge Status ... ');
551
552
		$conflicts = $this->_workingCopyConflictTracker->getNewConflicts($wc_path);
553
554
		if ( !$conflicts ) {
1 ignored issue
show
Bug Best Practice introduced by
The expression $conflicts 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...
555
			$this->io->writeln('<info>Successful</info>');
556
557
			return;
558
		}
559
560
		$this->_workingCopyConflictTracker->add($wc_path);
561
		$this->io->writeln('<error>' . count($conflicts) . ' conflict(-s)</error>');
562
563
		$table = new Table($this->io->getOutput());
564
565
		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 zero. 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...
566
			$table->setHeaders(array(
567
				'Path',
568
				'Associated Revisions (before ' . $largest_suggested_revision . ')',
569
			));
570
		}
571
		else {
572
			$table->setHeaders(array(
573
				'Path',
574
				'Associated Revisions',
575
			));
576
		}
577
578
		$revision_log = $this->getRevisionLog($source_url);
579
		$source_path = $this->repositoryConnector->getRelativePath($source_url) . '/';
580
581
		/** @var OutputHelper $output_helper */
582
		$output_helper = $this->getHelper('output');
583
584
		foreach ( $conflicts as $conflict_path ) {
585
			$path_revisions = $revision_log->find('paths', $source_path . $conflict_path);
586
			$path_revisions = array_intersect($this->_unmergedRevisions, $path_revisions);
587
588
			if ( $path_revisions && isset($largest_suggested_revision) ) {
1 ignored issue
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...
589
				$path_revisions = $this->limitRevisions($path_revisions, $largest_suggested_revision);
590
			}
591
592
			$table->addRow(array(
593
				$conflict_path,
594
				$path_revisions ? $output_helper->formatArray($path_revisions, 4) : '-',
595
			));
596
		}
597
598
		$table->render();
599
600
		throw new CommandException('Working copy contains unresolved merge conflicts.');
601
	}
602
603
	/**
604
	 * Returns revisions not larger, then given one.
605
	 *
606
	 * @param array   $revisions    Revisions.
607
	 * @param integer $max_revision Maximal revision.
608
	 *
609
	 * @return array
610
	 */
611
	protected function limitRevisions(array $revisions, $max_revision)
612
	{
613
		$ret = array();
614
615
		foreach ( $revisions as $revision ) {
616
			if ( $revision < $max_revision ) {
617
				$ret[] = $revision;
618
			}
619
		}
620
621
		return $ret;
622
	}
623
624
	/**
625
	 * Performs commit unless user doesn't want it.
626
	 *
627
	 * @return void
628
	 */
629
	protected function performCommit()
630
	{
631
		$auto_commit = $this->io->getOption('auto-commit');
632
633
		if ( $auto_commit !== null ) {
634
			$auto_commit = $auto_commit === 'yes';
635
		}
636
		else {
637
			$auto_commit = (boolean)$this->getSetting(self::SETTING_MERGE_AUTO_COMMIT);
638
		}
639
640
		if ( $auto_commit ) {
641
			$this->io->writeln(array('', 'Commencing automatic commit after merge ...'));
642
			$this->runOtherCommand('commit');
643
		}
644
	}
645
646
	/**
647
	 * Returns list of config settings.
648
	 *
649
	 * @return AbstractConfigSetting[]
650
	 */
651
	public function getConfigSettings()
652
	{
653
		return array(
654
			new StringConfigSetting(self::SETTING_MERGE_SOURCE_URL, ''),
655
			new ArrayConfigSetting(self::SETTING_MERGE_RECENT_CONFLICTS, array()),
656
			new ChoiceConfigSetting(
657
				self::SETTING_MERGE_AUTO_COMMIT,
658
				array(1 => 'Yes', 0 => 'No'),
659
				1
660
			),
661
		);
662
	}
663
664
	/**
665
	 * Returns option names, that makes sense to use in aggregation mode.
666
	 *
667
	 * @return array
668
	 */
669
	public function getAggregatedOptions()
670
	{
671
		return array('with-details', 'with-summary');
672
	}
673
674
}
675