Failed Conditions
Push — master ( 30a496...e276e6 )
by Alexander
02:31
created

LogCommand::getMergeConflictPrediction()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30
Metric Value
dl 0
loc 18
ccs 0
cts 6
cp 0
rs 8.8571
cc 5
eloc 9
nc 5
nop 2
crap 30
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
		$this
75
			->setName('log')
76
			->setDescription(
77
				'Show the log messages for a set of revisions, bugs, paths, refs, etc.'
78
			)
79
			->addArgument(
80
				'path',
81
				InputArgument::OPTIONAL,
82
				'Working copy path or URL',
83
				'.'
84
			)
85
			->addOption(
86
				'revisions',
87
				'r',
88
				InputOption::VALUE_REQUIRED,
89
				'List of revision(-s) and/or revision range(-s), e.g. <comment>53324</comment>, <comment>1224-4433</comment>'
90
			)
91
			->addOption(
92
				'bugs',
93
				'b',
94
				InputOption::VALUE_REQUIRED,
95
				'List of bug(-s), e.g. <comment>JRA-1234</comment>, <comment>43644</comment>'
96
			)
97
			->addOption(
98
				'refs',
99
				null,
100
				InputOption::VALUE_REQUIRED,
101
				'List of refs, e.g. <comment>trunk</comment>, <comment>branches/branch-name</comment>, <comment>tags/tag-name</comment>'
102
			)
103
			->addOption(
104
				'merges',
105
				null,
106
				InputOption::VALUE_NONE,
107
				'Show merge revisions only'
108
			)
109
			->addOption(
110
				'no-merges',
111
				null,
112
				InputOption::VALUE_NONE,
113
				'Hide merge revisions'
114
			)
115
			->addOption(
116
				'merged',
117
				null,
118
				InputOption::VALUE_NONE,
119
				'Shows only revisions, that were merged at least once'
120
			)
121
			->addOption(
122
				'not-merged',
123
				null,
124
				InputOption::VALUE_NONE,
125
				'Shows only revisions, that were not merged'
126
			)
127
			->addOption(
128
				'merged-by',
129
				null,
130
				InputOption::VALUE_REQUIRED,
131
				'Show revisions merged by list of revision(-s) and/or revision range(-s)'
132
			)
133
			->addOption(
134
				'with-details',
135
				'd',
136
				InputOption::VALUE_NONE,
137
				'Shows detailed revision information, e.g. paths affected'
138
			)
139
			->addOption(
140
				'with-summary',
141
				's',
142
				InputOption::VALUE_NONE,
143
				'Shows number of added/changed/removed paths in the revision'
144
			)
145
			->addOption(
146
				'with-refs',
147
				null,
148
				InputOption::VALUE_NONE,
149
				'Shows revision refs'
150
			)
151
			->addOption(
152
				'with-merge-oracle',
153
				null,
154
				InputOption::VALUE_NONE,
155
				'Shows number of paths in the revision, that can cause conflict upon merging'
156
			)
157
			->addOption(
158
				'with-merge-status',
159
				null,
160
				InputOption::VALUE_NONE,
161
				'Shows merge revisions affecting this revision'
162
			)
163
			->addOption(
164
				'max-count',
165
				null,
166
				InputOption::VALUE_REQUIRED,
167
				'Limit the number of revisions to output'
168
			);
169
170
		parent::configure();
171
	}
172
173
	/**
174
	 * Return possible values for the named option
175
	 *
176
	 * @param string            $optionName Option name.
177
	 * @param CompletionContext $context    Completion context.
178
	 *
179
	 * @return array
180
	 */
181
	public function completeOptionValues($optionName, CompletionContext $context)
182
	{
183
		$ret = parent::completeOptionValues($optionName, $context);
184
185
		if ( $optionName === 'refs' ) {
186
			return $this->getAllRefs();
187
		}
188
189
		return $ret;
190
	}
191
192
	/**
193
	 * {@inheritdoc}
194
	 */
195
	public function initialize(InputInterface $input, OutputInterface $output)
196
	{
197
		parent::initialize($input, $output);
198
199
		$this->_revisionLog = $this->getRevisionLog($this->getWorkingCopyUrl());
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
	 */
0 ignored issues
show
Coding Style Documentation introduced by
Missing @throws tag in function comment
Loading history...
205
	protected function execute(InputInterface $input, OutputInterface $output)
206
	{
207
		$bugs = $this->getList($this->io->getOption('bugs'));
208
		$revisions = $this->getList($this->io->getOption('revisions'));
209
210
		if ( $bugs && $revisions ) {
211
			throw new \RuntimeException('The "--bugs" and "--revisions" options are mutually exclusive.');
212
		}
213
214
		$missing_revisions = array();
215
		$revisions_by_path = $this->getRevisionsByPath();
216
217
		if ( $revisions ) {
218
			$revisions = $this->_revisionListParser->expandRanges($revisions);
219
			$revisions_by_path = array_intersect($revisions_by_path, $revisions);
220
			$missing_revisions = array_diff($revisions, $revisions_by_path);
221
		}
222
		elseif ( $bugs ) {
223
			// Only show bug-related revisions on given path. The $missing_revisions is always empty.
224
			$revisions_from_bugs = $this->_revisionLog->find('bugs', $bugs);
225
			$revisions_by_path = array_intersect($revisions_by_path, $revisions_from_bugs);
226
		}
227
228
		$merged_by = $this->getList($this->io->getOption('merged-by'));
229
230
		if ( $merged_by ) {
231
			// Exclude revisions, that were merged outside of project root folder in repository.
232
			$merged_by = $this->_revisionListParser->expandRanges($merged_by);
233
			$revisions_by_path = $this->_revisionLog->find(
234
				'paths',
235
				$this->repositoryConnector->getProjectUrl(
236
					$this->repositoryConnector->getRelativePath($this->getWorkingCopyPath())
237
				)
238
			);
239
			$revisions_by_path = array_intersect($revisions_by_path, $this->_revisionLog->find('merges', $merged_by));
240
		}
241
242
		if ( $this->io->getOption('merges') ) {
243
			$revisions_by_path = array_intersect($revisions_by_path, $this->_revisionLog->find('merges', 'all_merges'));
244
		}
245
		elseif ( $this->io->getOption('no-merges') ) {
246
			$revisions_by_path = array_diff($revisions_by_path, $this->_revisionLog->find('merges', 'all_merges'));
247
		}
248
249
		if ( $this->io->getOption('merged') ) {
250
			$revisions_by_path = array_intersect($revisions_by_path, $this->_revisionLog->find('merges', 'all_merged'));
251
		}
252
		elseif ( $this->io->getOption('not-merged') ) {
253
			$revisions_by_path = array_diff($revisions_by_path, $this->_revisionLog->find('merges', 'all_merged'));
254
		}
255
256
		if ( $missing_revisions ) {
257
			throw new CommandException($this->getMissingRevisionsErrorMessage($missing_revisions));
258
		}
259
		elseif ( !$revisions_by_path ) {
260
			throw new CommandException('No matching revisions found.');
261
		}
262
263
		rsort($revisions_by_path, SORT_NUMERIC);
264
265
		if ( $bugs || $revisions ) {
266
			// Don't limit revisions, when provided explicitly by user.
267
			$revisions_by_path_with_limit = $revisions_by_path;
268
		}
269
		else {
270
			// Apply limit only, when no explicit bugs/revisions are set.
271
			$revisions_by_path_with_limit = array_slice($revisions_by_path, 0, $this->getMaxCount());
272
		}
273
274
		$revisions_by_path_count = count($revisions_by_path);
275
		$revisions_by_path_with_limit_count = count($revisions_by_path_with_limit);
276
277
		if ( $revisions_by_path_with_limit_count === $revisions_by_path_count ) {
278
			$this->io->writeln(sprintf(
279
				' * Showing <info>%d</info> revision(-s):',
280
				$revisions_by_path_with_limit_count
281
			));
282
		}
283
		else {
284
			$this->io->writeln(sprintf(
285
				' * Showing <info>%d</info> of <info>%d</info> revision(-s):',
286
				$revisions_by_path_with_limit_count,
287
				$revisions_by_path_count
288
			));
289
		}
290
291
		$this->printRevisions($revisions_by_path_with_limit, (boolean)$this->io->getOption('with-details'));
292
	}
293
294
	/**
295
	 * Shows error about missing revisions.
296
	 *
297
	 * @param array $missing_revisions Missing revisions.
298
	 *
299
	 * @return string
300
	 */
301
	protected function getMissingRevisionsErrorMessage(array $missing_revisions)
302
	{
303
		$refs = $this->io->getOption('refs');
304
		$missing_revisions = implode(', ', $missing_revisions);
305
306
		if ( $refs ) {
307
			$revision_source = 'in "' . $refs . '" ref(-s)';
308
		}
309
		else {
310
			$revision_source = 'at "' . $this->getWorkingCopyUrl() . '" url';
311
		}
312
313
		return 'The ' . $missing_revisions . ' revision(-s) not found ' . $revision_source . '.';
314
	}
315
316
	/**
317
	 * Returns list of revisions by path.
318
	 *
319
	 * @return array
320
	 * @throws CommandException When given refs doesn't exist.
321
	 */
322
	protected function getRevisionsByPath()
323
	{
324
		$refs = $this->getList($this->io->getOption('refs'));
325
		$relative_path = $this->repositoryConnector->getRelativePath($this->getWorkingCopyPath());
326
327
		if ( !$refs ) {
328
			$ref = $this->repositoryConnector->getRefByPath($relative_path);
329
330
			// Use search by ref, when working copy represents ref root folder.
331
			if ( $ref !== false && preg_match('/' . preg_quote($ref, '/') . '$/', $relative_path) ) {
332
				return $this->_revisionLog->find('refs', $ref);
333
			}
334
		}
335
336
		if ( $refs ) {
337
			$incorrect_refs = array_diff($refs, $this->getAllRefs());
338
339
			if ( $incorrect_refs ) {
340
				throw new CommandException(
341
					'The following refs are unknown: "' . implode('", "', $incorrect_refs) . '".'
342
				);
343
			}
344
345
			return $this->_revisionLog->find('refs', $refs);
346
		}
347
348
		return $this->_revisionLog->find('paths', $relative_path);
349
	}
350
351
	/**
352
	 * Returns displayed revision limit.
353
	 *
354
	 * @return integer
355
	 */
356
	protected function getMaxCount()
357
	{
358
		$max_count = $this->io->getOption('max-count');
359
360
		if ( $max_count !== null ) {
361
			return $max_count;
362
		}
363
364
		return $this->getSetting(self::SETTING_LOG_LIMIT);
365
	}
366
367
	/**
368
	 * Prints revisions.
369
	 *
370
	 * @param array   $revisions    Revisions.
371
	 * @param boolean $with_details Print extended revision details (e.g. paths changed).
372
	 *
373
	 * @return void
374
	 */
375
	protected function printRevisions(array $revisions, $with_details = false)
376
	{
377
		$table = new Table($this->io->getOutput());
378
		$headers = array('Revision', 'Author', 'Date', 'Bug-ID', 'Log Message');
379
380
		// Add "Summary" header.
381
		$with_summary = $this->io->getOption('with-summary');
382
383
		if ( $with_summary ) {
384
			$headers[] = 'Summary';
385
		}
386
387
		// Add "Refs" header.
388
		$with_refs = $this->io->getOption('with-refs');
389
390
		if ( $with_refs ) {
391
			$headers[] = 'Refs';
392
		}
393
394
		$with_merge_oracle = $this->io->getOption('with-merge-oracle');
395
396
		// Add "M.O." header.
397
		if ( $with_merge_oracle ) {
398
			$headers[] = 'M.O.';
399
			$merge_conflict_regexps = $this->getMergeConflictRegExps();
400
		}
401
402
		// Add "Merged Via" header.
403
		$with_merge_status = $this->io->getOption('with-merge-status');
404
405
		if ( $with_merge_status ) {
406
			$headers[] = 'Merged Via';
407
		}
408
409
		$table->setHeaders($headers);
410
411
		/** @var DateHelper $date_helper */
412
		$date_helper = $this->getHelper('date');
413
414
		$prev_bugs = null;
415
		$last_color = 'yellow';
416
		$last_revision = end($revisions);
417
418
		$relative_wc_path = $this->repositoryConnector->getRelativePath(
419
			$this->getWorkingCopyPath()
420
		) . '/';
421
		$project_path = $this->repositoryConnector->getProjectUrl($relative_wc_path) . '/';
422
423
		$log_message_limit = $this->getSetting(self::SETTING_LOG_MESSAGE_LIMIT);
424
		$bugs_per_row = $with_details ? 1 : 3;
425
426
		$revisions_data = $this->_revisionLog->getRevisionsData('summary', $revisions);
427
		$revisions_paths = $this->_revisionLog->getRevisionsData('paths', $revisions);
428
		$revisions_bugs = $this->_revisionLog->getRevisionsData('bugs', $revisions);
429
		$revisions_refs = $this->_revisionLog->getRevisionsData('refs', $revisions);
430
431
		if ( $with_merge_status ) {
432
			$revisions_merged_via = $this->_revisionLog->getRevisionsData('merges', $revisions);
433
			$revisions_merged_via_refs = $this->_revisionLog->getRevisionsData(
434
				'refs',
435
				call_user_func_array('array_merge', $revisions_merged_via)
436
			);
437
		}
438
439
		foreach ( $revisions as $revision ) {
440
			$revision_data = $revisions_data[$revision];
441
442
			if ( $with_details ) {
443
				// When details requested don't transform commit message except for word wrapping.
444
				$log_message = wordwrap($revision_data['msg'], $log_message_limit); // FIXME: Not UTF-8 safe solution.
445
			}
446
			else {
447
				// When details not requested only operate on first line of commit message.
448
				list($log_message,) = explode(PHP_EOL, $revision_data['msg']);
449
				$log_message = preg_replace('/^\[fixes:.*?\]/s', "\xE2\x9C\x94", $log_message);
450
451
				if ( strpos($revision_data['msg'], PHP_EOL) !== false
452
					|| mb_strlen($log_message) > $log_message_limit
453
				) {
454
					$log_message = mb_substr($log_message, 0, $log_message_limit - 3) . '...';
455
				}
456
			}
457
458
			$new_bugs = $revisions_bugs[$revision];
459
460
			if ( isset($prev_bugs) && $new_bugs !== $prev_bugs ) {
461
				$last_color = $last_color == 'yellow' ? 'magenta' : 'yellow';
462
			}
463
464
			$row = array(
465
				$revision,
466
				$revision_data['author'],
467
				$date_helper->getAgoTime($revision_data['date']),
468
				$this->formatArray($new_bugs, $bugs_per_row, $last_color),
469
				$log_message,
470
			);
471
472
			$revision_paths = $revisions_paths[$revision];
473
474
			// Add "Summary" column.
475
			if ( $with_summary ) {
476
				$row[] = $this->generateChangeSummary($revision_paths);
477
			}
478
479
			// Add "Refs" column.
480
			if ( $with_refs ) {
481
				$row[] = $this->formatArray(
482
					$revisions_refs[$revision],
483
					1
484
				);
485
			}
486
487
			// Add "M.O." column.
488
			if ( $with_merge_oracle ) {
489
				$merge_conflict_predication = $this->getMergeConflictPrediction(
490
					$revision_paths,
491
					$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...
492
				);
493
				$row[] = $merge_conflict_predication ? '<error>' . count($merge_conflict_predication) . '</error>' : '';
494
			}
495
			else {
496
				$merge_conflict_predication = array();
497
			}
498
499
			// Add "Merged Via" column.
500
			if ( $with_merge_status ) {
501
				$row[] = $this->generateMergedVia($revisions_merged_via[$revision], $revisions_merged_via_refs);
2 ignored issues
show
Bug introduced by
The variable $revisions_merged_via 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...
Bug introduced by
The variable $revisions_merged_via_refs 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...
502
			}
503
504
			$table->addRow($row);
505
506
			if ( $with_details ) {
507
				$details = '<fg=white;options=bold>Changed Paths:</>';
508
				$path_cut_off_regexp = $this->getPathCutOffRegExp($project_path, $revisions_refs[$revision]);
509
510
				foreach ( $revision_paths as $path_data ) {
511
					$path_action = $path_data['action'];
512
					$relative_path = $this->_getRelativeLogPath($path_data, 'path', $path_cut_off_regexp);
513
514
					$details .= PHP_EOL . ' * ';
515
516
					if ( $path_action == 'A' ) {
517
						$color_format = 'fg=green';
518
					}
519
					elseif ( $path_action == 'D' ) {
520
						$color_format = 'fg=red';
521
					}
522
					else {
523
						$color_format = in_array($path_data['path'], $merge_conflict_predication) ? 'error' : '';
524
					}
525
526
					$to_colorize = array($path_action . '    ' . $relative_path);
527
528
					if ( isset($path_data['copyfrom-path']) ) {
529
						// TODO: When copy happened from different ref/project, then relative path = absolute path.
530
						$copy_from_rev = $path_data['copyfrom-rev'];
531
						$copy_from_path = $this->_getRelativeLogPath($path_data, 'copyfrom-path', $path_cut_off_regexp);
532
						$to_colorize[] = '        (from ' . $copy_from_path . ':' . $copy_from_rev . ')';
533
					}
534
535
					if ( $color_format ) {
536
						$details .= '<' . $color_format . '>';
537
						$details .= implode('</>' . PHP_EOL . '<' . $color_format . '>', $to_colorize);
538
						$details .= '</>';
539
					}
540
					else {
541
						$details .= implode(PHP_EOL, $to_colorize);
542
					}
543
				}
544
545
				$table->addRow(new TableSeparator());
546
				$table->addRow(array(new TableCell($details, array('colspan' => 5))));
547
548
				if ( $revision != $last_revision ) {
549
					$table->addRow(new TableSeparator());
550
				}
551
			}
552
553
			$prev_bugs = $new_bugs;
554
		}
555
556
		$table->render();
557
	}
558
559
	/**
560
	 * Returns formatted list of records.
561
	 *
562
	 * @param array       $items         List of items.
563
	 * @param integer     $items_per_row Number of bugs displayed per row.
564
	 * @param string|null $color         Color.
565
	 *
566
	 * @return string
567
	 */
568
	protected function formatArray(array $items, $items_per_row, $color = null)
569
	{
570
		$bug_chunks = array_chunk($items, $items_per_row);
571
572
		$ret = array();
573
574
		if ( isset($color) ) {
575
			foreach ( $bug_chunks as $bug_chunk ) {
576
				$ret[] = '<fg=' . $color . '>' . implode('</>, <fg=' . $color . '>', $bug_chunk) . '</>';
577
			}
578
		}
579
		else {
580
			foreach ( $bug_chunks as $bug_chunk ) {
581
				$ret[] = implode(', ', $bug_chunk);
582
			}
583
		}
584
585
		return implode(',' . PHP_EOL, $ret);
586
	}
587
588
	/**
589
	 * Generates change summary for a revision.
590
	 *
591
	 * @param array $revision_paths Revision paths.
592
	 *
593
	 * @return string
594
	 */
595
	protected function generateChangeSummary(array $revision_paths)
596
	{
597
		$summary = array('added' => 0, 'changed' => 0, 'removed' => 0);
598
599
		foreach ( $revision_paths as $path_data ) {
600
			$path_action = $path_data['action'];
601
602
			if ( $path_action == 'A' ) {
603
				$summary['added']++;
604
			}
605
			elseif ( $path_action == 'D' ) {
606
				$summary['removed']++;
607
			}
608
			else {
609
				$summary['changed']++;
610
			}
611
		}
612
613
		if ( $summary['added'] ) {
614
			$summary['added'] = '<fg=green>+' . $summary['added'] . '</>';
615
		}
616
617
		if ( $summary['removed'] ) {
618
			$summary['removed'] = '<fg=red>-' . $summary['removed'] . '</>';
619
		}
620
621
		return implode(' ', array_filter($summary));
622
	}
623
624
	/**
625
	 * Generates content for "Merged Via" cell content.
626
	 *
627
	 * @param array $merged_via                Merged Via.
628
	 * @param array $revisions_merged_via_refs Merged Via Refs.
629
	 *
630
	 * @return string
631
	 */
632
	protected function generateMergedVia(array $merged_via, array $revisions_merged_via_refs)
633
	{
634
		if ( !$merged_via ) {
635
			return '';
636
		}
637
638
		$merged_via_enhanced = array();
639
640
		foreach ( $merged_via as $merged_via_revision ) {
641
			$merged_via_revision_refs = $revisions_merged_via_refs[$merged_via_revision];
642
643
			if ( $merged_via_revision_refs ) {
644
				$merged_via_enhanced[] = $merged_via_revision . ' (' . implode(',', $merged_via_revision_refs) . ')';
645
			}
646
			else {
647
				$merged_via_enhanced[] = $merged_via_revision;
648
			}
649
		}
650
651
		return $this->formatArray($merged_via_enhanced, 1);
652
	}
653
654
	/**
655
	 * Returns merge conflict path predictions.
656
	 *
657
	 * @param array $revision_paths         Revision paths.
658
	 * @param array $merge_conflict_regexps Merge conflict paths.
659
	 *
660
	 * @return array
661
	 */
662
	protected function getMergeConflictPrediction(array $revision_paths, array $merge_conflict_regexps)
663
	{
664
		if ( !$merge_conflict_regexps ) {
665
			return array();
666
		}
667
668
		$conflict_paths = array();
669
670
		foreach ( $revision_paths as $revision_path ) {
671
			foreach ( $merge_conflict_regexps as $merge_conflict_regexp ) {
672
				if ( preg_match($merge_conflict_regexp, $revision_path['path']) ) {
673
					$conflict_paths[] = $revision_path['path'];
674
				}
675
			}
676
		}
677
678
		return $conflict_paths;
679
	}
680
681
	/**
682
	 * Returns merge conflict regexps.
683
	 *
684
	 * @return array
685
	 */
686
	protected function getMergeConflictRegExps()
687
	{
688
		return $this->getSetting(self::SETTING_LOG_MERGE_CONFLICT_REGEXPS);
689
	}
690
691
	/**
692
	 * Returns relative path to "svn log" returned path.
693
	 *
694
	 * @param array  $path_data           Path data.
695
	 * @param string $path_key            Path key.
696
	 * @param string $path_cut_off_regexp Path cut off regexp.
697
	 *
698
	 * @return string
699
	 */
700
	private function _getRelativeLogPath(array $path_data, $path_key, $path_cut_off_regexp)
701
	{
702
		$ret = $path_data[$path_key];
703
704
		if ( $path_data['kind'] == 'dir' ) {
705
			$ret .= '/';
706
		}
707
708
		$ret = preg_replace($path_cut_off_regexp, '', $ret, 1);
709
710
		if ( $ret === '' ) {
711
			$ret = '.';
712
		}
713
714
		return $ret;
715
	}
716
717
	/**
718
	 * Returns path cut off regexp.
719
	 *
720
	 * @param string $project_path Project path.
721
	 * @param array  $refs         Refs.
722
	 *
723
	 * @return string
724
	 */
725
	protected function getPathCutOffRegExp($project_path, array $refs)
726
	{
727
		$ret = array();
728
729
		// Remove ref from path only for single-ref revision.
730
		if ( count($refs) === 1 ) {
731
			$ret[] = $project_path . reset($refs) . '/';
732
		}
733
734
		// Always remove project path.
735
		$ret[] = $project_path;
736
737
		return '#^(' . implode('|', array_map('preg_quote', $ret)) . ')#';
738
	}
739
740
	/**
741
	 * Returns URL to the working copy.
742
	 *
743
	 * @return string
744
	 */
745
	protected function getWorkingCopyUrl()
746
	{
747
		$wc_path = $this->getWorkingCopyPath();
748
749
		if ( !$this->repositoryConnector->isUrl($wc_path) ) {
750
			return $this->repositoryConnector->getWorkingCopyUrl($wc_path);
751
		}
752
753
		return $wc_path;
754
	}
755
756
	/**
757
	 * Returns list of config settings.
758
	 *
759
	 * @return AbstractConfigSetting[]
760
	 */
761
	public function getConfigSettings()
762
	{
763
		return array(
764
			new IntegerConfigSetting(self::SETTING_LOG_LIMIT, 10),
765
			new IntegerConfigSetting(self::SETTING_LOG_MESSAGE_LIMIT, 68),
766
			new RegExpsConfigSetting(self::SETTING_LOG_MERGE_CONFLICT_REGEXPS, '#/composer\.lock$#'),
767
		);
768
	}
769
770
}
771