Failed Conditions
Push — master ( dd969d...42da22 )
by Alexander
10:30
created

MergeCommand::createMergeProgressBar()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 11
ccs 0
cts 8
cp 0
rs 10
cc 2
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
	 * 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
				'no-merges',
128
				null,
129
				InputOption::VALUE_NONE,
130
				'Hide merge revisions'
131
			)
132
			->addOption(
133
				'with-full-message',
134
				'f',
135
				InputOption::VALUE_NONE,
136
				'Shows non-truncated commit messages'
137
			)
138
			->addOption(
139
				'with-details',
140
				'd',
141
				InputOption::VALUE_NONE,
142
				'Shows detailed revision information, e.g. paths affected'
143
			)
144
			->addOption(
145
				'with-summary',
146
				's',
147
				InputOption::VALUE_NONE,
148
				'Shows number of added/changed/removed paths in the revision'
149
			)
150
			->addOption(
151
				'update-revision',
152
				null,
153
				InputOption::VALUE_REQUIRED,
154
				'Update working copy to given revision before performing a merge'
155
			)
156
			->addOption(
157
				'auto-commit',
158
				null,
159
				InputOption::VALUE_REQUIRED,
160
				'Automatically perform commit on successful merge, e.g. <comment>yes</comment> or <comment>no</comment>'
161
			)
162
			->addOption(
163
				'record-only',
164
				null,
165
				InputOption::VALUE_NONE,
166
				'Mark revisions as merged without actually merging them'
167
			)
168
			->addOption(
169
				'reverse',
170
				null,
171
				InputOption::VALUE_NONE,
172
				'Rollback previously merged revisions'
173
			)
174
			->addOption(
175
				'aggregate',
176
				null,
177
				InputOption::VALUE_NONE,
178
				'Aggregate displayed revisions by bugs'
179
			);
180
181
		parent::configure();
182
	}
183
184
	/**
185
	 * Return possible values for the named option
186
	 *
187
	 * @param string            $optionName Option name.
188
	 * @param CompletionContext $context    Completion context.
189
	 *
190
	 * @return array
191
	 */
192
	public function completeOptionValues($optionName, CompletionContext $context)
193
	{
194
		$ret = parent::completeOptionValues($optionName, $context);
195
196
		if ( $optionName === 'revisions' ) {
197
			return array('all');
198
		}
199
200
		if ( $optionName === 'source-url' ) {
201
			return $this->getAllRefs();
202
		}
203
204
		if ( $optionName === 'auto-commit' ) {
205
			return array('yes', 'no');
206
		}
207
208
		return $ret;
209
	}
210
211
	/**
212
	 * {@inheritdoc}
213
	 *
214
	 * @throws \RuntimeException When both "--bugs" and "--revisions" options were specified.
215
	 * @throws CommandException When everything is merged.
216
	 * @throws CommandException When manually specified revisions are already merged.
217
	 */
218
	protected function execute(InputInterface $input, OutputInterface $output)
219
	{
220
		$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

220
		$bugs = $this->getList(/** @scrutinizer ignore-type */ $this->io->getOption('bugs'));
Loading history...
221
		$revisions = $this->getList($this->io->getOption('revisions'));
222
223
		if ( $bugs && $revisions ) {
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...
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...
224
			throw new \RuntimeException('The "--bugs" and "--revisions" options are mutually exclusive.');
225
		}
226
227
		$wc_path = $this->getWorkingCopyPath();
228
229
		$this->ensureLatestWorkingCopy($wc_path);
230
231
		$source_url = $this->getSourceUrl($wc_path);
232
		$this->printSourceAndTarget($source_url, $wc_path);
233
		$this->_usableRevisions = $this->getUsableRevisions($source_url, $wc_path);
234
235
		if ( ($bugs || $revisions) && !$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...
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...
236
			throw new CommandException(\sprintf(
237
				'Nothing to %s.',
238
				$this->isReverseMerge() ? 'reverse-merge' : 'merge'
239
			));
240
		}
241
242
		$this->ensureWorkingCopyWithoutConflicts($source_url, $wc_path);
243
244
		if ( $this->shouldUseAll($revisions) ) {
245
			$revisions = $this->_usableRevisions;
246
247
			if ( $this->io->getOption('no-merges') ) {
248
				$revisions = array_diff($revisions, $this->getRevisionLog($source_url)->find('merges', 'all_merges'));
249
			}
250
		}
251
		else {
252
			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...
253
				$revisions = $this->getDirectRevisions($revisions, $source_url);
254
			}
255
			elseif ( $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...
256
				$revisions = $this->getRevisionLog($source_url)->find('bugs', $bugs);
257
258
				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...
259
					throw new CommandException('Specified bugs aren\'t mentioned in any of revisions');
260
				}
261
			}
262
263
			if ( $this->io->getOption('no-merges') ) {
264
				$revisions = array_diff($revisions, $this->getRevisionLog($source_url)->find('merges', 'all_merges'));
265
			}
266
267
			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...
268
				$revisions = array_intersect($revisions, $this->_usableRevisions);
269
270
				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...
271
					throw new CommandException(\sprintf(
272
						'Requested revisions are %s',
273
						$this->isReverseMerge() ? 'not yet merged' : 'already merged'
274
					));
275
				}
276
			}
277
		}
278
279
		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...
280
			$this->performMerge($source_url, $wc_path, $revisions);
281
		}
282
		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...
283
			$this->runOtherCommand('log', array(
284
				'path' => $source_url,
285
				'--revisions' => implode(',', $this->_usableRevisions),
286
				'--no-merges' => $this->io->getOption('no-merges'),
287
				'--with-full-message' => $this->io->getOption('with-full-message'),
288
				'--with-details' => $this->io->getOption('with-details'),
289
				'--with-summary' => $this->io->getOption('with-summary'),
290
				'--aggregate' => $this->io->getOption('aggregate'),
291
				'--with-merge-oracle' => true,
292
			));
293
		}
294
	}
295
296
	/**
297
	 * Determines if all usable revisions should be processed.
298
	 *
299
	 * @param array $revisions Revisions.
300
	 *
301
	 * @return boolean
302
	 */
303
	protected function shouldUseAll(array $revisions)
304
	{
305
		return $revisions === array(self::REVISION_ALL);
306
	}
307
308
	/**
309
	 * Ensures, that working copy is up to date.
310
	 *
311
	 * @param string $wc_path Working copy path.
312
	 *
313
	 * @return void
314
	 */
315
	protected function ensureLatestWorkingCopy($wc_path)
316
	{
317
		$this->io->write(' * Working Copy Status ... ');
318
		$update_revision = $this->io->getOption('update-revision');
319
320
		if ( $this->repositoryConnector->getWorkingCopyMissing($wc_path) ) {
321
			$this->io->writeln('<error>Locally deleted files found</error>');
322
			$this->updateWorkingCopy($wc_path, $update_revision);
323
324
			return;
325
		}
326
327
		$working_copy_revisions = $this->repositoryConnector->getWorkingCopyRevisions($wc_path);
328
329
		if ( count($working_copy_revisions) > 1 ) {
330
			$this->io->writeln(
331
				'<error>Mixed revisions: ' . implode(', ', $working_copy_revisions) . '</error>'
332
			);
333
			$this->updateWorkingCopy($wc_path, $update_revision);
334
335
			return;
336
		}
337
338
		$update_revision = $this->getWorkingCopyUpdateRevision($wc_path);
339
340
		if ( isset($update_revision) ) {
341
			$this->io->writeln('<error>Not at ' . $update_revision . ' revision</error>');
342
			$this->updateWorkingCopy($wc_path, $update_revision);
343
344
			return;
345
		}
346
347
		$this->io->writeln('<info>Up to date</info>');
348
	}
349
350
	/**
351
	 * Returns revision, that working copy needs to be updated to.
352
	 *
353
	 * @param string $wc_path Working copy path.
354
	 *
355
	 * @return integer|null
356
	 */
357
	protected function getWorkingCopyUpdateRevision($wc_path)
358
	{
359
		$update_revision = $this->io->getOption('update-revision');
360
		$actual_revision = $this->repositoryConnector->getLastRevision($wc_path);
361
362
		if ( isset($update_revision) ) {
363
			if ( is_numeric($update_revision) ) {
364
				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...
365
			}
366
367
			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...
368
		}
369
370
		$repository_revision = $this->repositoryConnector->getLastRevision(
371
			$this->repositoryConnector->getWorkingCopyUrl($wc_path)
372
		);
373
374
		return $repository_revision > $actual_revision ? $repository_revision : null;
375
	}
376
377
	/**
378
	 * Updates working copy.
379
	 *
380
	 * @param string     $wc_path  Working copy path.
381
	 * @param mixed|null $revision Revision.
382
	 *
383
	 * @return void
384
	 */
385
	protected function updateWorkingCopy($wc_path, $revision = null)
386
	{
387
		$arguments = array('path' => $wc_path, '--ignore-externals' => true);
388
389
		if ( isset($revision) ) {
390
			$arguments['--revision'] = $revision;
391
		}
392
393
		$this->runOtherCommand('update', $arguments);
394
	}
395
396
	/**
397
	 * Returns source url for merge.
398
	 *
399
	 * @param string $wc_path Working copy path.
400
	 *
401
	 * @return string
402
	 * @throws CommandException When source path is invalid.
403
	 */
404
	protected function getSourceUrl($wc_path)
405
	{
406
		$source_url = $this->io->getOption('source-url');
407
408
		if ( $source_url === null ) {
409
			$source_url = $this->getSetting(self::SETTING_MERGE_SOURCE_URL);
410
		}
411
		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

411
		elseif ( !$this->repositoryConnector->isUrl(/** @scrutinizer ignore-type */ $source_url) ) {
Loading history...
412
			$wc_url = $this->repositoryConnector->getWorkingCopyUrl($wc_path);
413
			$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

413
			$source_url = $this->_urlResolver->resolve($wc_url, /** @scrutinizer ignore-type */ $source_url);
Loading history...
414
		}
415
416
		if ( !$source_url ) {
417
			$wc_url = $this->repositoryConnector->getWorkingCopyUrl($wc_path);
418
			$source_url = $this->_mergeSourceDetector->detect($wc_url);
419
420
			if ( $source_url ) {
421
				$this->setSetting(self::SETTING_MERGE_SOURCE_URL, $source_url);
422
			}
423
		}
424
425
		if ( !$source_url ) {
426
			$wc_url = $this->repositoryConnector->getWorkingCopyUrl($wc_path);
427
			$error_msg = 'Unable to guess "--source-url" option value. Please specify it manually.' . PHP_EOL;
428
			$error_msg .= 'Working Copy URL: ' . $wc_url . '.';
429
			throw new CommandException($error_msg);
430
		}
431
432
		return $source_url;
433
	}
434
435
	/**
436
	 * Prints information about merge source & target.
437
	 *
438
	 * @param string $source_url Merge source: url.
439
	 * @param string $wc_path    Merge target: working copy path.
440
	 *
441
	 * @return void
442
	 */
443
	protected function printSourceAndTarget($source_url, $wc_path)
444
	{
445
		$relative_source_url = $this->repositoryConnector->getRelativePath($source_url);
446
		$relative_target_url = $this->repositoryConnector->getRelativePath($wc_path);
447
448
		$this->io->writeln(' * Merge Source ... <info>' . $relative_source_url . '</info>');
449
		$this->io->writeln(' * Merge Target ... <info>' . $relative_target_url . '</info>');
450
	}
451
452
	/**
453
	 * Ensures, that there are some usable revisions.
454
	 *
455
	 * @param string $source_url Merge source: url.
456
	 * @param string $wc_path    Merge target: working copy path.
457
	 *
458
	 * @return array
459
	 */
460
	protected function getUsableRevisions($source_url, $wc_path)
461
	{
462
		// Avoid missing revision query progress bar overwriting following output.
463
		$revision_log = $this->getRevisionLog($source_url);
464
465
		$this->io->write(sprintf(
466
			' * Upcoming %s Status ... ',
467
			$this->isReverseMerge() ? 'Reverse-merge' : 'Merge'
468
		));
469
		$usable_revisions = $this->calculateUsableRevisions($source_url, $wc_path);
470
471
		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...
472
			$usable_bugs = $revision_log->getBugsFromRevisions($usable_revisions);
473
			$error_msg = '<error>%d revision(-s) or %d bug(-s) %s</error>';
474
			$this->io->writeln(sprintf(
475
				$error_msg,
476
				count($usable_revisions),
477
				count($usable_bugs),
478
				$this->isReverseMerge() ? 'merged' : 'not merged'
479
			));
480
		}
481
		else {
482
			$this->io->writeln('<info>Up to date</info>');
483
		}
484
485
		return $usable_revisions;
486
	}
487
488
	/**
489
	 * Returns usable revisions.
490
	 *
491
	 * @param string $source_url Merge source: url.
492
	 * @param string $wc_path    Merge target: working copy path.
493
	 *
494
	 * @return array
495
	 */
496
	protected function calculateUsableRevisions($source_url, $wc_path)
497
	{
498
		$command = $this->repositoryConnector->getCommand(
499
			'mergeinfo',
500
			sprintf(
501
				'--show-revs %s {%s} {%s}',
502
				$this->isReverseMerge() ? 'merged' : 'eligible',
503
				$source_url,
504
				$wc_path
505
			)
506
		);
507
508
		$merge_info = $this->repositoryConnector->getProperty('svn:mergeinfo', $wc_path);
509
510
		$cache_invalidator = array(
511
			'source:' . $this->repositoryConnector->getLastRevision($source_url),
512
			'merged_hash:' . crc32($merge_info),
513
		);
514
		$command->setCacheInvalidator(implode(';', $cache_invalidator));
515
516
		$merge_info = $command->run();
517
		$merge_info = explode(PHP_EOL, $merge_info);
518
519
		foreach ( $merge_info as $index => $revision ) {
520
			$merge_info[$index] = ltrim($revision, 'r');
521
		}
522
523
		return array_filter($merge_info);
524
	}
525
526
	/**
527
	 * Parses information from "svn:mergeinfo" property.
528
	 *
529
	 * @param string $source_path Merge source: path in repository.
530
	 * @param string $wc_path     Merge target: working copy path.
531
	 *
532
	 * @return array
533
	 */
534
	protected function getMergedRevisions($source_path, $wc_path)
535
	{
536
		$merge_info = $this->repositoryConnector->getProperty('svn:mergeinfo', $wc_path);
537
		$merge_info = array_filter(explode("\n", $merge_info));
538
539
		foreach ( $merge_info as $merge_info_line ) {
540
			list($path, $revisions) = explode(':', $merge_info_line, 2);
541
542
			if ( $path === $source_path ) {
543
				return $this->_revisionListParser->expandRanges(explode(',', $revisions));
544
			}
545
		}
546
547
		return array();
548
	}
549
550
	/**
551
	 * Validates revisions to actually exist.
552
	 *
553
	 * @param array  $revisions      Revisions.
554
	 * @param string $repository_url Repository url.
555
	 *
556
	 * @return array
557
	 * @throws CommandException When revision doesn't exist.
558
	 */
559
	protected function getDirectRevisions(array $revisions, $repository_url)
560
	{
561
		$revision_log = $this->getRevisionLog($repository_url);
562
563
		try {
564
			$revisions = $this->_revisionListParser->expandRanges($revisions);
565
			$revision_log->getRevisionsData('summary', $revisions);
566
		}
567
		catch ( \InvalidArgumentException $e ) {
568
			throw new CommandException($e->getMessage());
569
		}
570
571
		return $revisions;
572
	}
573
574
	/**
575
	 * Performs merge.
576
	 *
577
	 * @param string $source_url Merge source: url.
578
	 * @param string $wc_path    Merge target: working copy path.
579
	 * @param array  $revisions  Revisions to merge.
580
	 *
581
	 * @return void
582
	 */
583
	protected function performMerge($source_url, $wc_path, array $revisions)
584
	{
585
		if ( $this->isReverseMerge() ) {
586
			rsort($revisions, SORT_NUMERIC);
587
		}
588
		else {
589
			sort($revisions, SORT_NUMERIC);
590
		}
591
592
		$revision_count = count($revisions);
593
594
		$used_revision_count = 0;
595
		$used_revisions = $this->repositoryConnector->getMergedRevisionChanges($wc_path, !$this->isReverseMerge());
596
597
		if ( $used_revisions ) {
598
			$used_revisions = call_user_func_array('array_merge', $used_revisions);
599
			$used_revision_count = count($used_revisions);
600
			$revision_count += $used_revision_count;
601
		}
602
603
		$param_string_beginning = '-c ';
604
		$param_string_ending = '{' . $source_url . '} {' . $wc_path . '}';
605
606
		if ( $this->isReverseMerge() ) {
607
			$param_string_beginning .= '-';
608
		}
609
610
		if ( $this->io->getOption('record-only') ) {
611
			$param_string_ending = '--record-only ' . $param_string_ending;
612
		}
613
614
		$revision_title_mask = $this->getRevisionTitle($wc_path);
615
616
		foreach ( $revisions as $index => $revision ) {
617
			$command = $this->repositoryConnector->getCommand(
618
				'merge',
619
				$param_string_beginning . $revision . ' ' . $param_string_ending
620
			);
621
622
			$progress_bar = $this->createMergeProgressBar($used_revision_count + $index + 1, $revision_count);
623
			$merge_heading = PHP_EOL . '<fg=white;options=bold>';
624
			$merge_heading .= '--- $1 ' . \str_replace('{revision}', $revision, $revision_title_mask);
625
			$merge_heading .= " into '$2' " . $progress_bar . ':</>';
626
627
			$command->runLive(array(
628
				$wc_path => '.',
629
				'/--- (Merging|Reverse-merging) r' . $revision . " into '([^']*)':/" => $merge_heading,
630
			));
631
632
			$this->_usableRevisions = array_diff($this->_usableRevisions, array($revision));
633
			$this->ensureWorkingCopyWithoutConflicts($source_url, $wc_path, $revision);
634
		}
635
636
		$this->performCommit();
637
	}
638
639
	/**
640
	 * Returns revision title.
641
	 *
642
	 * @param string $wc_path Working copy path.
643
	 *
644
	 * @return string
645
	 */
646
	protected function getRevisionTitle($wc_path)
647
	{
648
		$arcanist_config_file = $wc_path . \DIRECTORY_SEPARATOR . '.arcconfig';
649
650
		if ( !\file_exists($arcanist_config_file) ) {
651
			return '<fg=white;options=underscore>{revision}</> revision';
652
		}
653
654
		$arcanist_config = \json_decode(\file_get_contents($arcanist_config_file), true);
655
656
		if ( !\is_array($arcanist_config)
657
			|| !isset($arcanist_config['repository.callsign'], $arcanist_config['phabricator.uri'])
658
		) {
659
			return '<fg=white;options=underscore>{revision}</> revision';
660
		}
661
662
		$revision_title = $arcanist_config['phabricator.uri'];
663
		$revision_title .= 'r' . $arcanist_config['repository.callsign'] . '{revision}';
664
665
		return '<fg=white;options=underscore>' . $revision_title . '</>';
666
	}
667
668
	/**
669
	 * Create merge progress bar.
670
	 *
671
	 * @param integer $current Current.
672
	 * @param integer $total   Total.
673
	 *
674
	 * @return string
675
	 */
676
	protected function createMergeProgressBar($current, $total)
677
	{
678
		$total_length = 28;
679
		$percent_used = floor(($current / $total) * 100);
680
		$length_used = floor(($total_length * $percent_used) / 100);
681
		$length_free = $total_length - $length_used;
682
683
		$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

683
		$ret = $length_used > 0 ? str_repeat('=', /** @scrutinizer ignore-type */ $length_used - 1) : '';
Loading history...
684
		$ret .= '>' . str_repeat('-', $length_free);
685
686
		return '[' . $ret . '] ' . $percent_used . '% (' . $current . ' of ' . $total . ')';
687
	}
688
689
	/**
690
	 * Ensures, that there are no unresolved conflicts in working copy.
691
	 *
692
	 * @param string  $source_url                 Source url.
693
	 * @param string  $wc_path                    Working copy path.
694
	 * @param integer $largest_suggested_revision Largest revision, that is suggested in error message.
695
	 *
696
	 * @return void
697
	 * @throws CommandException When merge conflicts detected.
698
	 */
699
	protected function ensureWorkingCopyWithoutConflicts($source_url, $wc_path, $largest_suggested_revision = null)
700
	{
701
		$this->io->write(' * Previous Merge Status ... ');
702
703
		$conflicts = $this->_workingCopyConflictTracker->getNewConflicts($wc_path);
704
705
		if ( !$conflicts ) {
706
			$this->io->writeln('<info>Successful</info>');
707
708
			return;
709
		}
710
711
		$this->_workingCopyConflictTracker->add($wc_path);
712
		$this->io->writeln('<error>' . count($conflicts) . ' conflict(-s)</error>');
713
714
		$table = new Table($this->io->getOutput());
715
716
		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...
717
			$table->setHeaders(array(
718
				'Path',
719
				'Associated Revisions (before ' . $largest_suggested_revision . ')',
720
			));
721
		}
722
		else {
723
			$table->setHeaders(array(
724
				'Path',
725
				'Associated Revisions',
726
			));
727
		}
728
729
		$revision_log = $this->getRevisionLog($source_url);
730
		$source_path = $this->repositoryConnector->getRelativePath($source_url) . '/';
731
732
		/** @var OutputHelper $output_helper */
733
		$output_helper = $this->getHelper('output');
734
735
		foreach ( $conflicts as $conflict_path ) {
736
			$path_revisions = $revision_log->find('paths', $source_path . $conflict_path);
737
			$path_revisions = array_intersect($this->_usableRevisions, $path_revisions);
738
739
			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...
740
				$path_revisions = $this->limitRevisions($path_revisions, $largest_suggested_revision);
741
			}
742
743
			$table->addRow(array(
744
				$conflict_path,
745
				$path_revisions ? $output_helper->formatArray($path_revisions, 4) : '-',
746
			));
747
		}
748
749
		$table->render();
750
751
		throw new CommandException('Working copy contains unresolved merge conflicts.');
752
	}
753
754
	/**
755
	 * Returns revisions not larger, then given one.
756
	 *
757
	 * @param array   $revisions    Revisions.
758
	 * @param integer $max_revision Maximal revision.
759
	 *
760
	 * @return array
761
	 */
762
	protected function limitRevisions(array $revisions, $max_revision)
763
	{
764
		$ret = array();
765
766
		foreach ( $revisions as $revision ) {
767
			if ( $revision < $max_revision ) {
768
				$ret[] = $revision;
769
			}
770
		}
771
772
		return $ret;
773
	}
774
775
	/**
776
	 * Performs commit unless user doesn't want it.
777
	 *
778
	 * @return void
779
	 */
780
	protected function performCommit()
781
	{
782
		$auto_commit = $this->io->getOption('auto-commit');
783
784
		if ( $auto_commit !== null ) {
785
			$auto_commit = $auto_commit === 'yes';
786
		}
787
		else {
788
			$auto_commit = (boolean)$this->getSetting(self::SETTING_MERGE_AUTO_COMMIT);
789
		}
790
791
		if ( $auto_commit ) {
792
			$this->io->writeln(array('', 'Commencing automatic commit after merge ...'));
793
			$this->runOtherCommand('commit');
794
		}
795
	}
796
797
	/**
798
	 * Returns list of config settings.
799
	 *
800
	 * @return AbstractConfigSetting[]
801
	 */
802
	public function getConfigSettings()
803
	{
804
		return array(
805
			new StringConfigSetting(self::SETTING_MERGE_SOURCE_URL, ''),
806
			new ArrayConfigSetting(self::SETTING_MERGE_RECENT_CONFLICTS, array()),
807
			new ChoiceConfigSetting(
808
				self::SETTING_MERGE_AUTO_COMMIT,
809
				array(1 => 'Yes', 0 => 'No'),
810
				1
811
			),
812
		);
813
	}
814
815
	/**
816
	 * Returns option names, that makes sense to use in aggregation mode.
817
	 *
818
	 * @return array
819
	 */
820
	public function getAggregatedOptions()
821
	{
822
		return array('with-full-message', 'with-details', 'with-summary');
823
	}
824
825
	/**
826
	 * Determines if merge should be done in opposite direction (unmerge).
827
	 *
828
	 * @return boolean
829
	 */
830
	protected function isReverseMerge()
831
	{
832
		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...
833
	}
834
835
}
836