Completed
Push — master ( ff821a...097dbe )
by Alexander
02:39
created

MergeCommand::getUsableRevisions()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 27
ccs 0
cts 16
cp 0
rs 9.488
c 0
b 0
f 0
cc 4
nc 2
nop 2
crap 20
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
	 * 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
				'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-full-message',
128
				'f',
129
				InputOption::VALUE_NONE,
130
				'Shows non-truncated commit messages'
131
			)
132
			->addOption(
133
				'with-details',
134
				'd',
135
				InputOption::VALUE_NONE,
136
				'Shows detailed revision information, e.g. paths affected'
137
			)
138
			->addOption(
139
				'with-summary',
140
				's',
141
				InputOption::VALUE_NONE,
142
				'Shows number of added/changed/removed paths in the revision'
143
			)
144
			->addOption(
145
				'update-revision',
146
				null,
147
				InputOption::VALUE_REQUIRED,
148
				'Update working copy to given revision before performing a merge'
149
			)
150
			->addOption(
151
				'auto-commit',
152
				null,
153
				InputOption::VALUE_REQUIRED,
154
				'Automatically perform commit on successful merge, e.g. <comment>yes</comment> or <comment>no</comment>'
155
			)
156
			->addOption(
157
				'record-only',
158
				null,
159
				InputOption::VALUE_NONE,
160
				'Mark revisions as merged without actually merging them'
161
			)
162
			->addOption(
163
				'reverse',
164
				null,
165
				InputOption::VALUE_NONE,
166
				'Rollback previously merged revisions'
167
			);
168
169
		parent::configure();
170
	}
171
172
	/**
173
	 * Return possible values for the named option
174
	 *
175
	 * @param string            $optionName Option name.
176
	 * @param CompletionContext $context    Completion context.
177
	 *
178
	 * @return array
179
	 */
180
	public function completeOptionValues($optionName, CompletionContext $context)
181
	{
182
		$ret = parent::completeOptionValues($optionName, $context);
183
184
		if ( $optionName === 'revisions' ) {
185
			return array('all');
186
		}
187
188
		if ( $optionName === 'source-url' ) {
189
			return $this->getAllRefs();
190
		}
191
192
		if ( $optionName === 'auto-commit' ) {
193
			return array('yes', 'no');
194
		}
195
196
		return $ret;
197
	}
198
199
	/**
200
	 * {@inheritdoc}
201
	 *
202
	 * @throws \RuntimeException When both "--bugs" and "--revisions" options were specified.
203
	 * @throws CommandException When everything is merged.
204
	 * @throws CommandException When manually specified revisions are already merged.
205
	 */
206
	protected function execute(InputInterface $input, OutputInterface $output)
207
	{
208
		$bugs = $this->getList($this->io->getOption('bugs'));
209
		$revisions = $this->getList($this->io->getOption('revisions'));
210
211
		if ( $bugs && $revisions ) {
212
			throw new \RuntimeException('The "--bugs" and "--revisions" options are mutually exclusive.');
213
		}
214
215
		$wc_path = $this->getWorkingCopyPath();
216
217
		$this->ensureLatestWorkingCopy($wc_path);
218
219
		$source_url = $this->getSourceUrl($wc_path);
220
		$this->printSourceAndTarget($source_url, $wc_path);
221
		$this->_usableRevisions = $this->getUsableRevisions($source_url, $wc_path);
222
223
		if ( ($bugs || $revisions) && !$this->_usableRevisions ) {
224
			throw new CommandException(\sprintf(
225
				'Nothing to %s.',
226
				$this->isReverseMerge() ? 'reverse-merge' : 'merge'
227
			));
228
		}
229
230
		$this->ensureWorkingCopyWithoutConflicts($source_url, $wc_path);
231
232
		if ( $this->shouldUseAll($revisions) ) {
233
			$revisions = $this->_usableRevisions;
234
		}
235
		else {
236
			if ( $revisions ) {
237
				$revisions = $this->getDirectRevisions($revisions, $source_url);
238
			}
239
			elseif ( $bugs ) {
240
				$revisions = $this->getRevisionLog($source_url)->find('bugs', $bugs);
241
242
				if ( !$revisions ) {
243
					throw new CommandException('Specified bugs aren\'t mentioned in any of revisions');
244
				}
245
			}
246
247
			if ( $revisions ) {
248
				$revisions = array_intersect($revisions, $this->_usableRevisions);
249
250
				if ( !$revisions ) {
251
					throw new CommandException(\sprintf(
252
						'Requested revisions are %s',
253
						$this->isReverseMerge() ? 'not yet merged' : 'already merged'
254
					));
255
				}
256
			}
257
		}
258
259
		if ( $revisions ) {
260
			$this->performMerge($source_url, $wc_path, $revisions);
261
		}
262
		elseif ( $this->_usableRevisions ) {
263
			$this->runOtherCommand('log', array(
264
				'path' => $source_url,
265
				'--revisions' => implode(',', $this->_usableRevisions),
266
				'--with-full-message' => $this->io->getOption('with-full-message'),
267
				'--with-details' => $this->io->getOption('with-details'),
268
				'--with-summary' => $this->io->getOption('with-summary'),
269
				'--with-merge-oracle' => true,
270
			));
271
		}
272
	}
273
274
	/**
275
	 * Determines if all usable revisions should be processed.
276
	 *
277
	 * @param array $revisions Revisions.
278
	 *
279
	 * @return boolean
280
	 */
281
	protected function shouldUseAll(array $revisions)
282
	{
283
		return $revisions === array(self::REVISION_ALL);
284
	}
285
286
	/**
287
	 * Ensures, that working copy is up to date.
288
	 *
289
	 * @param string $wc_path Working copy path.
290
	 *
291
	 * @return void
292
	 */
293
	protected function ensureLatestWorkingCopy($wc_path)
294
	{
295
		$this->io->write(' * Working Copy Status ... ');
296
		$update_revision = $this->io->getOption('update-revision');
297
298
		if ( $this->repositoryConnector->getWorkingCopyMissing($wc_path) ) {
299
			$this->io->writeln('<error>Locally deleted files found</error>');
300
			$this->updateWorkingCopy($wc_path, $update_revision);
301
302
			return;
303
		}
304
305
		$working_copy_revisions = $this->repositoryConnector->getWorkingCopyRevisions($wc_path);
306
307
		if ( count($working_copy_revisions) > 1 ) {
308
			$this->io->writeln(
309
				'<error>Mixed revisions: ' . implode(', ', $working_copy_revisions) . '</error>'
310
			);
311
			$this->updateWorkingCopy($wc_path, $update_revision);
312
313
			return;
314
		}
315
316
		$update_revision = $this->getWorkingCopyUpdateRevision($wc_path);
317
318
		if ( isset($update_revision) ) {
319
			$this->io->writeln('<error>Not at ' . $update_revision . ' revision</error>');
320
			$this->updateWorkingCopy($wc_path, $update_revision);
321
322
			return;
323
		}
324
325
		$this->io->writeln('<info>Up to date</info>');
326
	}
327
328
	/**
329
	 * Returns revision, that working copy needs to be updated to.
330
	 *
331
	 * @param string $wc_path Working copy path.
332
	 *
333
	 * @return integer|null
334
	 */
335
	protected function getWorkingCopyUpdateRevision($wc_path)
336
	{
337
		$update_revision = $this->io->getOption('update-revision');
338
		$actual_revision = $this->repositoryConnector->getLastRevision($wc_path);
339
340
		if ( isset($update_revision) ) {
341
			if ( is_numeric($update_revision) ) {
342
				return (int)$update_revision === (int)$actual_revision ? null : $update_revision;
343
			}
344
345
			return $update_revision;
346
		}
347
348
		$repository_revision = $this->repositoryConnector->getLastRevision(
349
			$this->repositoryConnector->getWorkingCopyUrl($wc_path)
350
		);
351
352
		return $repository_revision > $actual_revision ? $repository_revision : null;
353
	}
354
355
	/**
356
	 * Updates working copy.
357
	 *
358
	 * @param string     $wc_path  Working copy path.
359
	 * @param mixed|null $revision Revision.
360
	 *
361
	 * @return void
362
	 */
363
	protected function updateWorkingCopy($wc_path, $revision = null)
364
	{
365
		$arguments = array('path' => $wc_path, '--ignore-externals' => true);
366
367
		if ( isset($revision) ) {
368
			$arguments['--revision'] = $revision;
369
		}
370
371
		$this->runOtherCommand('update', $arguments);
372
	}
373
374
	/**
375
	 * Returns source url for merge.
376
	 *
377
	 * @param string $wc_path Working copy path.
378
	 *
379
	 * @return string
380
	 * @throws CommandException When source path is invalid.
381
	 */
382
	protected function getSourceUrl($wc_path)
383
	{
384
		$source_url = $this->io->getOption('source-url');
385
386
		if ( $source_url === null ) {
387
			$source_url = $this->getSetting(self::SETTING_MERGE_SOURCE_URL);
388
		}
389
		elseif ( !$this->repositoryConnector->isUrl($source_url) ) {
390
			$wc_url = $this->repositoryConnector->getWorkingCopyUrl($wc_path);
391
			$source_url = $this->_urlResolver->resolve($wc_url, $source_url);
392
		}
393
394
		if ( !$source_url ) {
395
			$wc_url = $this->repositoryConnector->getWorkingCopyUrl($wc_path);
396
			$source_url = $this->_mergeSourceDetector->detect($wc_url);
397
398
			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...
399
				$this->setSetting(self::SETTING_MERGE_SOURCE_URL, $source_url);
400
			}
401
		}
402
403
		if ( !$source_url ) {
404
			$wc_url = $this->repositoryConnector->getWorkingCopyUrl($wc_path);
405
			$error_msg = 'Unable to guess "--source-url" option value. Please specify it manually.' . PHP_EOL;
406
			$error_msg .= 'Working Copy URL: ' . $wc_url . '.';
407
			throw new CommandException($error_msg);
408
		}
409
410
		return $source_url;
411
	}
412
413
	/**
414
	 * Prints information about merge source & target.
415
	 *
416
	 * @param string $source_url Merge source: url.
417
	 * @param string $wc_path    Merge target: working copy path.
418
	 *
419
	 * @return void
420
	 */
421
	protected function printSourceAndTarget($source_url, $wc_path)
422
	{
423
		$relative_source_url = $this->repositoryConnector->getRelativePath($source_url);
424
		$relative_target_url = $this->repositoryConnector->getRelativePath($wc_path);
425
426
		$this->io->writeln(' * Merge Source ... <info>' . $relative_source_url . '</info>');
427
		$this->io->writeln(' * Merge Target ... <info>' . $relative_target_url . '</info>');
428
	}
429
430
	/**
431
	 * Ensures, that there are some usable revisions.
432
	 *
433
	 * @param string $source_url Merge source: url.
434
	 * @param string $wc_path    Merge target: working copy path.
435
	 *
436
	 * @return array
437
	 */
438
	protected function getUsableRevisions($source_url, $wc_path)
439
	{
440
		// Avoid missing revision query progress bar overwriting following output.
441
		$revision_log = $this->getRevisionLog($source_url);
442
443
		$this->io->write(sprintf(
444
			' * Upcoming %s Status ... ',
445
			$this->isReverseMerge() ? 'Reverse-merge' : 'Merge'
446
		));
447
		$usable_revisions = $this->calculateUsableRevisions($source_url, $wc_path);
448
449
		if ( $usable_revisions ) {
450
			$usable_bugs = $revision_log->getBugsFromRevisions($usable_revisions);
451
			$error_msg = '<error>%d revision(-s) or %d bug(-s) %s</error>';
452
			$this->io->writeln(sprintf(
453
				$error_msg,
454
				count($usable_revisions),
455
				count($usable_bugs),
456
				$this->isReverseMerge() ? 'merged' : 'not merged'
457
			));
458
		}
459
		else {
460
			$this->io->writeln('<info>Up to date</info>');
461
		}
462
463
		return $usable_revisions;
464
	}
465
466
	/**
467
	 * Returns usable revisions.
468
	 *
469
	 * @param string $source_url Merge source: url.
470
	 * @param string $wc_path    Merge target: working copy path.
471
	 *
472
	 * @return array
473
	 */
474
	protected function calculateUsableRevisions($source_url, $wc_path)
475
	{
476
		$command = $this->repositoryConnector->getCommand(
477
			'mergeinfo',
478
			sprintf(
479
				'--show-revs %s {%s} {%s}',
480
				$this->isReverseMerge() ? 'merged' : 'eligible',
481
				$source_url,
482
				$wc_path
483
			)
484
		);
485
486
		$merge_info = $this->repositoryConnector->getProperty('svn:mergeinfo', $wc_path);
487
488
		$cache_invalidator = array(
489
			'source:' . $this->repositoryConnector->getLastRevision($source_url),
490
			'merged_hash:' . crc32($merge_info),
491
		);
492
		$command->setCacheInvalidator(implode(';', $cache_invalidator));
493
494
		$merge_info = $command->run();
495
		$merge_info = explode(PHP_EOL, $merge_info);
496
497
		foreach ( $merge_info as $index => $revision ) {
498
			$merge_info[$index] = ltrim($revision, 'r');
499
		}
500
501
		return array_filter($merge_info);
502
	}
503
504
	/**
505
	 * Parses information from "svn:mergeinfo" property.
506
	 *
507
	 * @param string $source_path Merge source: path in repository.
508
	 * @param string $wc_path     Merge target: working copy path.
509
	 *
510
	 * @return array
511
	 */
512
	protected function getMergedRevisions($source_path, $wc_path)
513
	{
514
		$merge_info = $this->repositoryConnector->getProperty('svn:mergeinfo', $wc_path);
515
		$merge_info = array_filter(explode("\n", $merge_info));
516
517
		foreach ( $merge_info as $merge_info_line ) {
518
			list($path, $revisions) = explode(':', $merge_info_line, 2);
519
520
			if ( $path === $source_path ) {
521
				return $this->_revisionListParser->expandRanges(explode(',', $revisions));
522
			}
523
		}
524
525
		return array();
526
	}
527
528
	/**
529
	 * Validates revisions to actually exist.
530
	 *
531
	 * @param array  $revisions      Revisions.
532
	 * @param string $repository_url Repository url.
533
	 *
534
	 * @return array
535
	 * @throws CommandException When revision doesn't exist.
536
	 */
537
	protected function getDirectRevisions(array $revisions, $repository_url)
538
	{
539
		$revision_log = $this->getRevisionLog($repository_url);
540
541
		try {
542
			$revisions = $this->_revisionListParser->expandRanges($revisions);
543
			$revision_log->getRevisionsData('summary', $revisions);
544
		}
545
		catch ( \InvalidArgumentException $e ) {
546
			throw new CommandException($e->getMessage());
547
		}
548
549
		return $revisions;
550
	}
551
552
	/**
553
	 * Performs merge.
554
	 *
555
	 * @param string $source_url Merge source: url.
556
	 * @param string $wc_path    Merge target: working copy path.
557
	 * @param array  $revisions  Revisions to merge.
558
	 *
559
	 * @return void
560
	 */
561
	protected function performMerge($source_url, $wc_path, array $revisions)
562
	{
563
		if ( $this->isReverseMerge() ) {
564
			rsort($revisions, SORT_NUMERIC);
565
		}
566
		else {
567
			sort($revisions, SORT_NUMERIC);
568
		}
569
570
		$revision_count = count($revisions);
571
572
		$used_revision_count = 0;
573
		$used_revisions = $this->repositoryConnector->getMergedRevisionChanges($wc_path, !$this->isReverseMerge());
574
575
		if ( $used_revisions ) {
576
			$used_revisions = call_user_func_array('array_merge', $used_revisions);
577
			$used_revision_count = count($used_revisions);
578
			$revision_count += $used_revision_count;
579
		}
580
581
		$param_string_beginning = '-c ';
582
		$param_string_ending = '{' . $source_url . '} {' . $wc_path . '}';
583
584
		if ( $this->isReverseMerge() ) {
585
			$param_string_beginning .= '-';
586
		}
587
588
		if ( $this->io->getOption('record-only') ) {
589
			$param_string_ending = '--record-only ' . $param_string_ending;
590
		}
591
592
		$revision_title_mask = $this->getRevisionTitle($wc_path);
593
594
		foreach ( $revisions as $index => $revision ) {
595
			$command = $this->repositoryConnector->getCommand(
596
				'merge',
597
				$param_string_beginning . $revision . ' ' . $param_string_ending
598
			);
599
600
			$progress_bar = $this->createMergeProgressBar($used_revision_count + $index + 1, $revision_count);
601
			$merge_heading = PHP_EOL . '<fg=white;options=bold>';
602
			$merge_heading .= '--- $1 ' . \str_replace('{revision}', $revision, $revision_title_mask);
603
			$merge_heading .= " into '$2' " . $progress_bar . ':</>';
604
605
			$command->runLive(array(
606
				$wc_path => '.',
607
				'/--- (Merging|Reverse-merging) r' . $revision . " into '([^']*)':/" => $merge_heading,
608
			));
609
610
			$this->_usableRevisions = array_diff($this->_usableRevisions, array($revision));
611
			$this->ensureWorkingCopyWithoutConflicts($source_url, $wc_path, $revision);
612
		}
613
614
		$this->performCommit();
615
	}
616
617
	/**
618
	 * Returns revision title.
619
	 *
620
	 * @param string $wc_path Working copy path.
621
	 *
622
	 * @return string
623
	 */
624
	protected function getRevisionTitle($wc_path)
625
	{
626
		$arcanist_config_file = $wc_path . \DIRECTORY_SEPARATOR . '.arcconfig';
627
628
		if ( !\file_exists($arcanist_config_file) ) {
629
			return '<fg=white;options=underscore>{revision}</> revision';
630
		}
631
632
		$arcanist_config = \json_decode(\file_get_contents($arcanist_config_file), true);
633
634
		if ( !\is_array($arcanist_config)
635
			|| !isset($arcanist_config['repository.callsign'], $arcanist_config['phabricator.uri'])
636
		) {
637
			return '<fg=white;options=underscore>{revision}</> revision';
638
		}
639
640
		$revision_title = $arcanist_config['phabricator.uri'];
641
		$revision_title .= 'r' . $arcanist_config['repository.callsign'] . '{revision}';
642
643
		return '<fg=white;options=underscore>' . $revision_title . '</>';
644
	}
645
646
	/**
647
	 * Create merge progress bar.
648
	 *
649
	 * @param integer $current Current.
650
	 * @param integer $total   Total.
651
	 *
652
	 * @return string
653
	 */
654
	protected function createMergeProgressBar($current, $total)
655
	{
656
		$total_length = 28;
657
		$percent_used = floor(($current / $total) * 100);
658
		$length_used = floor(($total_length * $percent_used) / 100);
659
		$length_free = $total_length - $length_used;
660
661
		$ret = $length_used > 0 ? str_repeat('=', $length_used - 1) : '';
662
		$ret .= '>' . str_repeat('-', $length_free);
663
664
		return '[' . $ret . '] ' . $percent_used . '% (' . $current . ' of ' . $total . ')';
665
	}
666
667
	/**
668
	 * Ensures, that there are no unresolved conflicts in working copy.
669
	 *
670
	 * @param string  $source_url                 Source url.
671
	 * @param string  $wc_path                    Working copy path.
672
	 * @param integer $largest_suggested_revision Largest revision, that is suggested in error message.
673
	 *
674
	 * @return void
675
	 * @throws CommandException When merge conflicts detected.
676
	 */
677
	protected function ensureWorkingCopyWithoutConflicts($source_url, $wc_path, $largest_suggested_revision = null)
678
	{
679
		$this->io->write(' * Previous Merge Status ... ');
680
681
		$conflicts = $this->_workingCopyConflictTracker->getNewConflicts($wc_path);
682
683
		if ( !$conflicts ) {
684
			$this->io->writeln('<info>Successful</info>');
685
686
			return;
687
		}
688
689
		$this->_workingCopyConflictTracker->add($wc_path);
690
		$this->io->writeln('<error>' . count($conflicts) . ' conflict(-s)</error>');
691
692
		$table = new Table($this->io->getOutput());
693
694
		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...
695
			$table->setHeaders(array(
696
				'Path',
697
				'Associated Revisions (before ' . $largest_suggested_revision . ')',
698
			));
699
		}
700
		else {
701
			$table->setHeaders(array(
702
				'Path',
703
				'Associated Revisions',
704
			));
705
		}
706
707
		$revision_log = $this->getRevisionLog($source_url);
708
		$source_path = $this->repositoryConnector->getRelativePath($source_url) . '/';
709
710
		/** @var OutputHelper $output_helper */
711
		$output_helper = $this->getHelper('output');
712
713
		foreach ( $conflicts as $conflict_path ) {
714
			$path_revisions = $revision_log->find('paths', $source_path . $conflict_path);
715
			$path_revisions = array_intersect($this->_usableRevisions, $path_revisions);
716
717
			if ( $path_revisions && isset($largest_suggested_revision) ) {
718
				$path_revisions = $this->limitRevisions($path_revisions, $largest_suggested_revision);
719
			}
720
721
			$table->addRow(array(
722
				$conflict_path,
723
				$path_revisions ? $output_helper->formatArray($path_revisions, 4) : '-',
724
			));
725
		}
726
727
		$table->render();
728
729
		throw new CommandException('Working copy contains unresolved merge conflicts.');
730
	}
731
732
	/**
733
	 * Returns revisions not larger, then given one.
734
	 *
735
	 * @param array   $revisions    Revisions.
736
	 * @param integer $max_revision Maximal revision.
737
	 *
738
	 * @return array
739
	 */
740
	protected function limitRevisions(array $revisions, $max_revision)
741
	{
742
		$ret = array();
743
744
		foreach ( $revisions as $revision ) {
745
			if ( $revision < $max_revision ) {
746
				$ret[] = $revision;
747
			}
748
		}
749
750
		return $ret;
751
	}
752
753
	/**
754
	 * Performs commit unless user doesn't want it.
755
	 *
756
	 * @return void
757
	 */
758
	protected function performCommit()
759
	{
760
		$auto_commit = $this->io->getOption('auto-commit');
761
762
		if ( $auto_commit !== null ) {
763
			$auto_commit = $auto_commit === 'yes';
764
		}
765
		else {
766
			$auto_commit = (boolean)$this->getSetting(self::SETTING_MERGE_AUTO_COMMIT);
767
		}
768
769
		if ( $auto_commit ) {
770
			$this->io->writeln(array('', 'Commencing automatic commit after merge ...'));
771
			$this->runOtherCommand('commit');
772
		}
773
	}
774
775
	/**
776
	 * Returns list of config settings.
777
	 *
778
	 * @return AbstractConfigSetting[]
779
	 */
780
	public function getConfigSettings()
781
	{
782
		return array(
783
			new StringConfigSetting(self::SETTING_MERGE_SOURCE_URL, ''),
784
			new ArrayConfigSetting(self::SETTING_MERGE_RECENT_CONFLICTS, array()),
785
			new ChoiceConfigSetting(
786
				self::SETTING_MERGE_AUTO_COMMIT,
787
				array(1 => 'Yes', 0 => 'No'),
788
				1
789
			),
790
		);
791
	}
792
793
	/**
794
	 * Returns option names, that makes sense to use in aggregation mode.
795
	 *
796
	 * @return array
797
	 */
798
	public function getAggregatedOptions()
799
	{
800
		return array('with-full-message', 'with-details', 'with-summary');
801
	}
802
803
	/**
804
	 * Determines if merge should be done in opposite direction (unmerge).
805
	 *
806
	 * @return boolean
807
	 */
808
	protected function isReverseMerge()
809
	{
810
		return $this->io->getOption('reverse');
811
	}
812
813
}
814