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 ) { |
|
|
|
|
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 ) { |
|
|
|
|
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 ) { |
|
|
|
|
222
|
|
|
$revisions = $this->getDirectRevisions($revisions, $source_url); |
223
|
|
|
} |
224
|
|
|
elseif ( $bugs ) { |
|
|
|
|
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 ) { |
|
|
|
|
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 ) { |
|
|
|
|
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 ) { |
|
|
|
|
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 ) { |
|
|
|
|
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 ) { |
|
|
|
|
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 ) { |
|
|
|
|
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) ) { |
|
|
|
|
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
|
|
|
|
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.