Completed
Push — master ( 5e49cf...3d1f7c )
by Alexander
02:26
created

MergeCommand::getUnmergedRevisions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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