Completed
Push — master ( 8d3483...76eec6 )
by Alexander
02:52
created

LogCommand::generateMergedVia()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 23
ccs 0
cts 13
cp 0
rs 8.7972
cc 4
eloc 12
nc 4
nop 1
crap 20
1
<?php
2
/**
3
 * This file is part of the SVN-Buddy library.
4
 * For the full copyright and license information, please view
5
 * the LICENSE file that was distributed with this source code.
6
 *
7
 * @copyright Alexander Obuhovich <[email protected]>
8
 * @link      https://github.com/console-helpers/svn-buddy
9
 */
10
11
namespace ConsoleHelpers\SVNBuddy\Command;
12
13
14
use ConsoleHelpers\SVNBuddy\Config\AbstractConfigSetting;
15
use ConsoleHelpers\SVNBuddy\Config\IntegerConfigSetting;
16
use ConsoleHelpers\SVNBuddy\Config\RegExpsConfigSetting;
17
use ConsoleHelpers\ConsoleKit\Exception\CommandException;
18
use ConsoleHelpers\SVNBuddy\Helper\DateHelper;
19
use ConsoleHelpers\SVNBuddy\Repository\Parser\RevisionListParser;
20
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\RevisionLog;
21
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
22
use Symfony\Component\Console\Helper\Table;
23
use Symfony\Component\Console\Helper\TableCell;
24
use Symfony\Component\Console\Helper\TableSeparator;
25
use Symfony\Component\Console\Input\InputArgument;
26
use Symfony\Component\Console\Input\InputInterface;
27
use Symfony\Component\Console\Input\InputOption;
28
use Symfony\Component\Console\Output\OutputInterface;
29
30
class LogCommand extends AbstractCommand implements IAggregatorAwareCommand, IConfigAwareCommand
31
{
32
33
	const SETTING_LOG_LIMIT = 'log.limit';
34
35
	const SETTING_LOG_MESSAGE_LIMIT = 'log.message-limit';
36
37
	const SETTING_LOG_MERGE_CONFLICT_REGEXPS = 'log.merge-conflict-regexps';
38
39
	/**
40
	 * Revision list parser.
41
	 *
42
	 * @var RevisionListParser
43
	 */
44
	private $_revisionListParser;
45
46
	/**
47
	 * Revision log
48
	 *
49
	 * @var RevisionLog
50
	 */
51
	private $_revisionLog;
52
53
	/**
54
	 * Prepare dependencies.
55
	 *
56
	 * @return void
57
	 */
58
	protected function prepareDependencies()
59
	{
60
		parent::prepareDependencies();
61
62
		$container = $this->getContainer();
63
64
		$this->_revisionListParser = $container['revision_list_parser'];
65
	}
66
67
	/**
68
	 * {@inheritdoc}
0 ignored issues
show
introduced by
Doc comment short description must start with a capital letter
Loading history...
69
	 */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
70
	protected function configure()
71
	{
72
		$this->pathAcceptsUrl = true;
73
74
		$description = <<<TEXT
75
<info>NOTE:</info>
76
77
The revision is considered merged only, when associated merge revision
78
can be found. Unfortunately Subversion doesn't create merge revisions
79
on direct path operations (e.g. replacing <comment>tags/stable</comment> with <comment>trunk</comment>) and
80
therefore affected revisions won't be considered as merged by SVN-Buddy.
81
TEXT;
82
83
		$this
84
			->setName('log')
85
			->setDescription(
86
				'Show the log messages for a set of revisions, bugs, paths, refs, etc.'
87
			)
88
			->setHelp($description)
89
			->addArgument(
90
				'path',
91
				InputArgument::OPTIONAL,
92
				'Working copy path or URL',
93
				'.'
94
			)
95
			->addOption(
96
				'revisions',
97
				'r',
98
				InputOption::VALUE_REQUIRED,
99
				'List of revision(-s) and/or revision range(-s), e.g. <comment>53324</comment>, <comment>1224-4433</comment>'
100
			)
101
			->addOption(
102
				'bugs',
103
				'b',
104
				InputOption::VALUE_REQUIRED,
105
				'List of bug(-s), e.g. <comment>JRA-1234</comment>, <comment>43644</comment>'
106
			)
107
			->addOption(
108
				'refs',
109
				null,
110
				InputOption::VALUE_REQUIRED,
111
				'List of refs, e.g. <comment>trunk</comment>, <comment>branches/branch-name</comment>, <comment>tags/tag-name</comment>'
112
			)
113
			->addOption(
114
				'merges',
115
				null,
116
				InputOption::VALUE_NONE,
117
				'Show merge revisions only'
118
			)
119
			->addOption(
120
				'no-merges',
121
				null,
122
				InputOption::VALUE_NONE,
123
				'Hide merge revisions'
124
			)
125
			->addOption(
126
				'merged',
127
				null,
128
				InputOption::VALUE_NONE,
129
				'Shows only revisions, that were merged at least once'
130
			)
131
			->addOption(
132
				'not-merged',
133
				null,
134
				InputOption::VALUE_NONE,
135
				'Shows only revisions, that were not merged'
136
			)
137
			->addOption(
138
				'merged-by',
139
				null,
140
				InputOption::VALUE_REQUIRED,
141
				'Show revisions merged by list of revision(-s) and/or revision range(-s)'
142
			)
143
			->addOption(
144
				'with-details',
145
				'd',
146
				InputOption::VALUE_NONE,
147
				'Shows detailed revision information, e.g. paths affected'
148
			)
149
			->addOption(
150
				'with-summary',
151
				's',
152
				InputOption::VALUE_NONE,
153
				'Shows number of added/changed/removed paths in the revision'
154
			)
155
			->addOption(
156
				'with-refs',
157
				null,
158
				InputOption::VALUE_NONE,
159
				'Shows revision refs'
160
			)
161
			->addOption(
162
				'with-merge-oracle',
163
				null,
164
				InputOption::VALUE_NONE,
165
				'Shows number of paths in the revision, that can cause conflict upon merging'
166
			)
167
			->addOption(
168
				'with-merge-status',
169
				null,
170
				InputOption::VALUE_NONE,
171
				'Shows merge revisions affecting this revision'
172
			)
173
			->addOption(
174
				'max-count',
175
				null,
176
				InputOption::VALUE_REQUIRED,
177
				'Limit the number of revisions to output'
178
			);
179
180
		parent::configure();
181
	}
182
183
	/**
184
	 * Return possible values for the named option
185
	 *
186
	 * @param string            $optionName Option name.
187
	 * @param CompletionContext $context    Completion context.
188
	 *
189
	 * @return array
190
	 */
191
	public function completeOptionValues($optionName, CompletionContext $context)
192
	{
193
		$ret = parent::completeOptionValues($optionName, $context);
194
195
		if ( $optionName === 'refs' ) {
196
			return $this->getAllRefs();
197
		}
198
199
		return $ret;
200
	}
201
202
	/**
0 ignored issues
show
introduced by
Doc comment for parameter "$input" missing
Loading history...
introduced by
Doc comment for parameter "$output" missing
Loading history...
203
	 * {@inheritdoc}
204
	 */
205
	public function initialize(InputInterface $input, OutputInterface $output)
206
	{
207
		parent::initialize($input, $output);
208
209
		$this->_revisionLog = $this->getRevisionLog($this->getWorkingCopyUrl());
210
	}
211
212
	/**
213
	 * {@inheritdoc}
0 ignored issues
show
introduced by
Doc comment short description must start with a capital letter
Loading history...
214
	 */
0 ignored issues
show
Coding Style Documentation introduced by
Missing @throws tag in function comment
Loading history...
introduced by
Missing @return tag in function comment
Loading history...
215
	protected function execute(InputInterface $input, OutputInterface $output)
216
	{
217
		$bugs = $this->getList($this->io->getOption('bugs'));
218
		$revisions = $this->getList($this->io->getOption('revisions'));
219
220
		if ( $bugs && $revisions ) {
221
			throw new \RuntimeException('The "--bugs" and "--revisions" options are mutually exclusive.');
222
		}
223
224
		$missing_revisions = array();
225
		$revisions_by_path = $this->getRevisionsByPath();
226
227
		if ( $revisions ) {
228
			$revisions = $this->_revisionListParser->expandRanges($revisions);
229
			$revisions_by_path = array_intersect($revisions_by_path, $revisions);
230
			$missing_revisions = array_diff($revisions, $revisions_by_path);
231
		}
232
		elseif ( $bugs ) {
233
			// Only show bug-related revisions on given path. The $missing_revisions is always empty.
234
			$revisions_from_bugs = $this->_revisionLog->find('bugs', $bugs);
235
			$revisions_by_path = array_intersect($revisions_by_path, $revisions_from_bugs);
236
		}
237
238
		$merged_by = $this->getList($this->io->getOption('merged-by'));
239
240
		if ( $merged_by ) {
241
			// Exclude revisions, that were merged outside of project root folder in repository.
242
			$merged_by = $this->_revisionListParser->expandRanges($merged_by);
243
			$revisions_by_path = $this->_revisionLog->find(
244
				'paths',
245
				$this->repositoryConnector->getProjectUrl(
246
					$this->repositoryConnector->getRelativePath($this->getWorkingCopyPath())
247
				)
248
			);
249
			$revisions_by_path = array_intersect($revisions_by_path, $this->_revisionLog->find('merges', $merged_by));
250
		}
251
252
		if ( $this->io->getOption('merges') ) {
253
			$revisions_by_path = array_intersect($revisions_by_path, $this->_revisionLog->find('merges', 'all_merges'));
254
		}
255
		elseif ( $this->io->getOption('no-merges') ) {
256
			$revisions_by_path = array_diff($revisions_by_path, $this->_revisionLog->find('merges', 'all_merges'));
257
		}
258
259
		if ( $this->io->getOption('merged') ) {
260
			$revisions_by_path = array_intersect($revisions_by_path, $this->_revisionLog->find('merges', 'all_merged'));
261
		}
262
		elseif ( $this->io->getOption('not-merged') ) {
263
			$revisions_by_path = array_diff($revisions_by_path, $this->_revisionLog->find('merges', 'all_merged'));
264
		}
265
266
		if ( $missing_revisions ) {
267
			throw new CommandException($this->getMissingRevisionsErrorMessage($missing_revisions));
268
		}
269
		elseif ( !$revisions_by_path ) {
270
			throw new CommandException('No matching revisions found.');
271
		}
272
273
		rsort($revisions_by_path, SORT_NUMERIC);
274
275
		if ( $bugs || $revisions ) {
276
			// Don't limit revisions, when provided explicitly by user.
277
			$revisions_by_path_with_limit = $revisions_by_path;
278
		}
279
		else {
280
			// Apply limit only, when no explicit bugs/revisions are set.
281
			$revisions_by_path_with_limit = array_slice($revisions_by_path, 0, $this->getMaxCount());
282
		}
283
284
		$revisions_by_path_count = count($revisions_by_path);
285
		$revisions_by_path_with_limit_count = count($revisions_by_path_with_limit);
286
287
		if ( $revisions_by_path_with_limit_count === $revisions_by_path_count ) {
288
			$this->io->writeln(sprintf(
289
				' * Showing <info>%d</info> revision(-s):',
290
				$revisions_by_path_with_limit_count
291
			));
292
		}
293
		else {
294
			$this->io->writeln(sprintf(
295
				' * Showing <info>%d</info> of <info>%d</info> revision(-s):',
296
				$revisions_by_path_with_limit_count,
297
				$revisions_by_path_count
298
			));
299
		}
300
301
		$this->printRevisions($revisions_by_path_with_limit, (boolean)$this->io->getOption('with-details'));
302
	}
303
304
	/**
305
	 * Shows error about missing revisions.
306
	 *
307
	 * @param array $missing_revisions Missing revisions.
308
	 *
309
	 * @return string
310
	 */
311
	protected function getMissingRevisionsErrorMessage(array $missing_revisions)
312
	{
313
		$refs = $this->io->getOption('refs');
314
		$missing_revisions = implode(', ', $missing_revisions);
315
316
		if ( $refs ) {
317
			$revision_source = 'in "' . $refs . '" ref(-s)';
318
		}
319
		else {
320
			$revision_source = 'at "' . $this->getWorkingCopyUrl() . '" url';
321
		}
322
323
		return 'The ' . $missing_revisions . ' revision(-s) not found ' . $revision_source . '.';
324
	}
325
326
	/**
327
	 * Returns list of revisions by path.
328
	 *
329
	 * @return array
330
	 * @throws CommandException When given refs doesn't exist.
331
	 */
332
	protected function getRevisionsByPath()
333
	{
334
		$refs = $this->getList($this->io->getOption('refs'));
335
		$relative_path = $this->repositoryConnector->getRelativePath($this->getWorkingCopyPath());
336
337
		if ( !$refs ) {
338
			$ref = $this->repositoryConnector->getRefByPath($relative_path);
339
340
			// Use search by ref, when working copy represents ref root folder.
341
			if ( $ref !== false && preg_match('/' . preg_quote($ref, '/') . '$/', $relative_path) ) {
342
				return $this->_revisionLog->find('refs', $ref);
343
			}
344
		}
345
346
		if ( $refs ) {
347
			$incorrect_refs = array_diff($refs, $this->getAllRefs());
348
349
			if ( $incorrect_refs ) {
350
				throw new CommandException(
351
					'The following refs are unknown: "' . implode('", "', $incorrect_refs) . '".'
352
				);
353
			}
354
355
			return $this->_revisionLog->find('refs', $refs);
356
		}
357
358
		return $this->_revisionLog->find('paths', $relative_path);
359
	}
360
361
	/**
362
	 * Returns displayed revision limit.
363
	 *
364
	 * @return integer
365
	 */
366
	protected function getMaxCount()
367
	{
368
		$max_count = $this->io->getOption('max-count');
369
370
		if ( $max_count !== null ) {
371
			return $max_count;
372
		}
373
374
		return $this->getSetting(self::SETTING_LOG_LIMIT);
375
	}
376
377
	/**
378
	 * Prints revisions.
379
	 *
380
	 * @param array   $revisions    Revisions.
381
	 * @param boolean $with_details Print extended revision details (e.g. paths changed).
382
	 *
383
	 * @return void
384
	 */
385
	protected function printRevisions(array $revisions, $with_details = false)
386
	{
387
		$table = new Table($this->io->getOutput());
388
		$headers = array('Revision', 'Author', 'Date', 'Bug-ID', 'Log Message');
389
390
		// Add "Summary" header.
391
		$with_summary = $this->io->getOption('with-summary');
392
393
		if ( $with_summary ) {
394
			$headers[] = 'Summary';
395
		}
396
397
		// Add "Refs" header.
398
		$with_refs = $this->io->getOption('with-refs');
399
400
		if ( $with_refs ) {
401
			$headers[] = 'Refs';
402
		}
403
404
		$with_merge_oracle = $this->io->getOption('with-merge-oracle');
405
406
		// Add "M.O." header.
407
		if ( $with_merge_oracle ) {
408
			$headers[] = 'M.O.';
409
			$merge_conflict_regexps = $this->getMergeConflictRegExps();
410
		}
411
412
		// Add "Merged Via" header.
413
		$with_merge_status = $this->io->getOption('with-merge-status');
414
415
		if ( $with_merge_status ) {
416
			$headers[] = 'Merged Via';
417
		}
418
419
		$table->setHeaders($headers);
420
421
		/** @var DateHelper $date_helper */
422
		$date_helper = $this->getHelper('date');
423
424
		$prev_bugs = null;
425
		$last_color = 'yellow';
426
		$last_revision = end($revisions);
427
428
		$relative_wc_path = $this->repositoryConnector->getRelativePath(
429
			$this->getWorkingCopyPath()
430
		) . '/';
431
		$project_path = $this->repositoryConnector->getProjectUrl($relative_wc_path) . '/';
432
433
		$log_message_limit = $this->getSetting(self::SETTING_LOG_MESSAGE_LIMIT);
434
		$bugs_per_row = $with_details ? 1 : 3;
435
436
		foreach ( $revisions as $revision ) {
437
			$revision_data = $this->_revisionLog->getRevisionData('summary', $revision);
438
439
			if ( $with_details ) {
440
				// When details requested don't transform commit message except for word wrapping.
441
				$log_message = wordwrap($revision_data['msg'], $log_message_limit); // FIXME: Not UTF-8 safe solution.
442
			}
443
			else {
444
				// When details not requested only operate on first line of commit message.
445
				list($log_message,) = explode(PHP_EOL, $revision_data['msg']);
446
				$log_message = preg_replace('/^\[fixes:.*?\]/s', "\xE2\x9C\x94", $log_message);
447
448
				if ( strpos($revision_data['msg'], PHP_EOL) !== false
449
					|| mb_strlen($log_message) > $log_message_limit
450
				) {
451
					$log_message = mb_substr($log_message, 0, $log_message_limit - 3) . '...';
452
				}
453
			}
454
455
			$new_bugs = $this->_revisionLog->getRevisionData('bugs', $revision);
456
457
			if ( isset($prev_bugs) && $new_bugs !== $prev_bugs ) {
458
				$last_color = $last_color == 'yellow' ? 'magenta' : 'yellow';
459
			}
460
461
			$row = array(
462
				$revision,
463
				$revision_data['author'],
464
				$date_helper->getAgoTime($revision_data['date']),
465
				$this->formatArray($new_bugs, $bugs_per_row, $last_color),
466
				$log_message,
467
			);
468
469
			$revision_paths = $this->_revisionLog->getRevisionData('paths', $revision);
470
471
			// Add "Summary" column.
472
			if ( $with_summary ) {
473
				$row[] = $this->generateChangeSummary($revision_paths);
474
			}
475
476
			// Add "Refs" column.
477
			if ( $with_refs ) {
478
				$row[] = $this->formatArray(
479
					$this->_revisionLog->getRevisionData('refs', $revision),
480
					1
481
				);
482
			}
483
484
			// Add "M.O." column.
485
			if ( $with_merge_oracle ) {
486
				$merge_conflict_predication = $this->getMergeConflictPrediction(
487
					$revision_paths,
488
					$merge_conflict_regexps
0 ignored issues
show
Bug introduced by
The variable $merge_conflict_regexps does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
489
				);
490
				$row[] = $merge_conflict_predication ? '<error>' . count($merge_conflict_predication) . '</error>' : '';
491
			}
492
			else {
493
				$merge_conflict_predication = array();
494
			}
495
496
			// Add "Merged Via" column.
497
			if ( $with_merge_status ) {
498
				$row[] = $this->generateMergedVia($revision);
499
			}
500
501
			$table->addRow($row);
502
503
			if ( $with_details ) {
504
				$details = '<fg=white;options=bold>Changed Paths:</>';
505
				$path_cut_off_regexp = $this->getPathCutOffRegExp($project_path, $revision);
506
507
				foreach ( $revision_paths as $path_data ) {
508
					$path_action = $path_data['action'];
509
					$relative_path = $this->_getRelativeLogPath($path_data, 'path', $path_cut_off_regexp);
510
511
					$details .= PHP_EOL . ' * ';
512
513
					if ( $path_action == 'A' ) {
514
						$color_format = 'fg=green';
515
					}
516
					elseif ( $path_action == 'D' ) {
517
						$color_format = 'fg=red';
518
					}
519
					else {
520
						$color_format = in_array($path_data['path'], $merge_conflict_predication) ? 'error' : '';
521
					}
522
523
					$to_colorize = array($path_action . '    ' . $relative_path);
524
525
					if ( isset($path_data['copyfrom-path']) ) {
526
						// TODO: When copy happened from different ref/project, then relative path = absolute path.
527
						$copy_from_rev = $path_data['copyfrom-rev'];
528
						$copy_from_path = $this->_getRelativeLogPath($path_data, 'copyfrom-path', $path_cut_off_regexp);
529
						$to_colorize[] = '        (from ' . $copy_from_path . ':' . $copy_from_rev . ')';
530
					}
531
532
					if ( $color_format ) {
533
						$details .= '<' . $color_format . '>';
534
						$details .= implode('</>' . PHP_EOL . '<' . $color_format . '>', $to_colorize);
535
						$details .= '</>';
536
					}
537
					else {
538
						$details .= implode(PHP_EOL, $to_colorize);
539
					}
540
				}
541
542
				$table->addRow(new TableSeparator());
543
				$table->addRow(array(new TableCell($details, array('colspan' => 5))));
544
545
				if ( $revision != $last_revision ) {
546
					$table->addRow(new TableSeparator());
547
				}
548
			}
549
550
			$prev_bugs = $new_bugs;
551
		}
552
553
		$table->render();
554
	}
555
556
	/**
557
	 * Returns formatted list of records.
558
	 *
559
	 * @param array       $items         List of items.
560
	 * @param integer     $items_per_row Number of bugs displayed per row.
561
	 * @param string|null $color         Color.
562
	 *
563
	 * @return string
564
	 */
565
	protected function formatArray(array $items, $items_per_row, $color = null)
566
	{
567
		$bug_chunks = array_chunk($items, $items_per_row);
568
569
		$ret = array();
570
571
		if ( isset($color) ) {
572
			foreach ( $bug_chunks as $bug_chunk ) {
573
				$ret[] = '<fg=' . $color . '>' . implode('</>, <fg=' . $color . '>', $bug_chunk) . '</>';
574
			}
575
		}
576
		else {
577
			foreach ( $bug_chunks as $bug_chunk ) {
578
				$ret[] = implode(', ', $bug_chunk);
579
			}
580
		}
581
582
		return implode(',' . PHP_EOL, $ret);
583
	}
584
585
	/**
586
	 * Generates change summary for a revision.
587
	 *
588
	 * @param array $revision_paths Revision paths.
589
	 *
590
	 * @return string
591
	 */
592
	protected function generateChangeSummary(array $revision_paths)
593
	{
594
		$summary = array('added' => 0, 'changed' => 0, 'removed' => 0);
595
596
		foreach ( $revision_paths as $path_data ) {
597
			$path_action = $path_data['action'];
598
599
			if ( $path_action == 'A' ) {
600
				$summary['added']++;
601
			}
602
			elseif ( $path_action == 'D' ) {
603
				$summary['removed']++;
604
			}
605
			else {
606
				$summary['changed']++;
607
			}
608
		}
609
610
		if ( $summary['added'] ) {
611
			$summary['added'] = '<fg=green>+' . $summary['added'] . '</>';
612
		}
613
614
		if ( $summary['removed'] ) {
615
			$summary['removed'] = '<fg=red>-' . $summary['removed'] . '</>';
616
		}
617
618
		return implode(' ', array_filter($summary));
619
	}
620
621
	/**
622
	 * Generates content for "Merged Via" cell content.
623
	 *
624
	 * @param integer $revision Revision.
625
	 *
626
	 * @return string
627
	 */
628
	protected function generateMergedVia($revision)
629
	{
630
		$merged_via = $this->_revisionLog->getRevisionData('merges', $revision);
631
632
		if ( !$merged_via ) {
633
			return '';
634
		}
635
636
		$merged_via_enhanced = array();
637
638
		foreach ( $merged_via as $merged_via_revision ) {
639
			$merged_via_revision_refs = $this->_revisionLog->getRevisionData('refs', $merged_via_revision);
640
641
			if ( $merged_via_revision_refs ) {
642
				$merged_via_enhanced[] = $merged_via_revision . ' (' . implode(',', $merged_via_revision_refs) . ')';
643
			}
644
			else {
645
				$merged_via_enhanced[] = $merged_via_revision;
646
			}
647
		}
648
649
		return $this->formatArray($merged_via_enhanced, 1);
650
	}
651
652
	/**
653
	 * Returns merge conflict path predictions.
654
	 *
655
	 * @param array $revision_paths         Revision paths.
656
	 * @param array $merge_conflict_regexps Merge conflict paths.
657
	 *
658
	 * @return array
659
	 */
660
	protected function getMergeConflictPrediction(array $revision_paths, array $merge_conflict_regexps)
661
	{
662
		if ( !$merge_conflict_regexps ) {
663
			return array();
664
		}
665
666
		$conflict_paths = array();
667
668
		foreach ( $revision_paths as $revision_path ) {
669
			foreach ( $merge_conflict_regexps as $merge_conflict_regexp ) {
670
				if ( preg_match($merge_conflict_regexp, $revision_path['path']) ) {
671
					$conflict_paths[] = $revision_path['path'];
672
				}
673
			}
674
		}
675
676
		return $conflict_paths;
677
	}
678
679
	/**
680
	 * Returns merge conflict regexps.
681
	 *
682
	 * @return array
683
	 */
684
	protected function getMergeConflictRegExps()
685
	{
686
		return $this->getSetting(self::SETTING_LOG_MERGE_CONFLICT_REGEXPS);
687
	}
688
689
	/**
690
	 * Returns relative path to "svn log" returned path.
691
	 *
692
	 * @param array  $path_data           Path data.
693
	 * @param string $path_key            Path key.
694
	 * @param string $path_cut_off_regexp Path cut off regexp.
695
	 *
696
	 * @return string
697
	 */
698
	private function _getRelativeLogPath(array $path_data, $path_key, $path_cut_off_regexp)
699
	{
700
		$ret = $path_data[$path_key];
701
702
		if ( $path_data['kind'] == 'dir' ) {
703
			$ret .= '/';
704
		}
705
706
		$ret = preg_replace($path_cut_off_regexp, '', $ret, 1);
707
708
		if ( $ret === '' ) {
709
			$ret = '.';
710
		}
711
712
		return $ret;
713
	}
714
715
	/**
716
	 * Returns path cut off regexp.
717
	 *
718
	 * @param string  $project_path Project path.
719
	 * @param integer $revision     Revision.
720
	 *
721
	 * @return string
722
	 */
723
	protected function getPathCutOffRegExp($project_path, $revision)
724
	{
725
		$ret = array();
726
		$refs = $this->_revisionLog->getRevisionData('refs', $revision);
727
728
		// Remove ref from path only for single-ref revision.
729
		if ( count($refs) === 1 ) {
730
			$ret[] = $project_path . reset($refs) . '/';
731
		}
732
733
		// Always remove project path.
734
		$ret[] = $project_path;
735
736
		return '#^(' . implode('|', array_map('preg_quote', $ret)) . ')#';
737
	}
738
739
	/**
740
	 * Returns URL to the working copy.
741
	 *
742
	 * @return string
743
	 */
744
	protected function getWorkingCopyUrl()
745
	{
746
		$wc_path = $this->getWorkingCopyPath();
747
748
		if ( !$this->repositoryConnector->isUrl($wc_path) ) {
749
			return $this->repositoryConnector->getWorkingCopyUrl($wc_path);
750
		}
751
752
		return $wc_path;
753
	}
754
755
	/**
756
	 * Returns list of config settings.
757
	 *
758
	 * @return AbstractConfigSetting[]
759
	 */
760
	public function getConfigSettings()
761
	{
762
		return array(
763
			new IntegerConfigSetting(self::SETTING_LOG_LIMIT, 10),
764
			new IntegerConfigSetting(self::SETTING_LOG_MESSAGE_LIMIT, 68),
765
			new RegExpsConfigSetting(self::SETTING_LOG_MERGE_CONFLICT_REGEXPS, '#/composer\.lock$#'),
766
		);
767
	}
768
769
}
770