Completed
Push — master ( 901199...b5bb32 )
by Alexander
02:35
created

LogCommand::prepareDependencies()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2
Metric Value
dl 0
loc 8
ccs 0
cts 5
cp 0
rs 9.4285
cc 1
eloc 4
nc 1
nop 0
crap 2
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_MERGE_CONFLICT_REGEXPS = 'log.merge-conflict-regexps';
36
37
	/**
38
	 * Revision list parser.
39
	 *
40
	 * @var RevisionListParser
41
	 */
42
	private $_revisionListParser;
43
44
	/**
45
	 * Revision log
46
	 *
47
	 * @var RevisionLog
48
	 */
49
	private $_revisionLog;
50
51
	/**
52
	 * Prepare dependencies.
53
	 *
54
	 * @return void
55
	 */
56
	protected function prepareDependencies()
57
	{
58
		parent::prepareDependencies();
59
60
		$container = $this->getContainer();
61
62
		$this->_revisionListParser = $container['revision_list_parser'];
63
	}
64
65
	/**
66
	 * {@inheritdoc}
0 ignored issues
show
introduced by
Doc comment short description must start with a capital letter
Loading history...
67
	 */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
68
	protected function configure()
69
	{
70
		$this->pathAcceptsUrl = true;
71
72
		$description = <<<TEXT
73
TODO
74
TEXT;
75
76
		$this
77
			->setName('log')
78
			->setDescription(
79
				'Show the log messages for revisions/bugs/path'
80
			)
81
			->setHelp($description)
82
			->addArgument(
83
				'path',
84
				InputArgument::OPTIONAL,
85
				'Working copy path or URL',
86
				'.'
87
			)
88
			->addOption(
89
				'revisions',
90
				'r',
91
				InputOption::VALUE_REQUIRED,
92
				'Revision or revision range (e.g. "53324,34342,1224-4433,232")'
93
			)
94
			->addOption(
95
				'bugs',
96
				'b',
97
				InputOption::VALUE_REQUIRED,
98
				'Bugs to merge (e.g. "JRA-1234,43644")'
99
			)
100
			->addOption(
101
				'refs',
102
				null,
103
				InputOption::VALUE_REQUIRED,
104
				'Refs (e.g. "trunk", "branches/branch-name", "tags/tag-name")'
105
			)
106
			->addOption(
107
				'details',
108
				'd',
109
				InputOption::VALUE_NONE,
110
				'Shows paths affected in each revision'
111
			)
112
			->addOption(
113
				'summary',
114
				's',
115
				InputOption::VALUE_NONE,
116
				'Shows summary of paths affected in each revision'
117
			)
118
			->addOption(
119
				'merge-oracle',
120
				null,
121
				InputOption::VALUE_NONE,
122
				'Detects commits with possible merge conflicts'
123
			)
124
			->addOption(
125
				'merges',
126
				null,
127
				InputOption::VALUE_NONE,
128
				'Print only merge commits'
129
			)
130
			->addOption(
131
				'no-merges',
132
				null,
133
				InputOption::VALUE_NONE,
134
				'Do not print merge commits'
135
			)
136
			->addOption(
137
				'merged',
138
				null,
139
				InputOption::VALUE_NONE,
140
				'Print only merged commits'
141
			)
142
			->addOption(
143
				'merged-by',
144
				null,
145
				InputOption::VALUE_REQUIRED,
146
				'Show revisions merged via given revision(-s)'
147
			)
148
			->addOption(
149
				'not-merged',
150
				null,
151
				InputOption::VALUE_NONE,
152
				'Print only not merged commits'
153
			)
154
			->addOption(
155
				'merge-status',
156
				null,
157
				InputOption::VALUE_NONE,
158
				'Show merge revisions (if any) for each revisions'
159
			)
160
			->addOption(
161
				'max-count',
162
				null,
163
				InputOption::VALUE_REQUIRED,
164
				'Limit the number of revisions to output'
165
			);
166
167
		parent::configure();
168
	}
169
170
	/**
171
	 * Return possible values for the named option
172
	 *
173
	 * @param string            $optionName Option name.
174
	 * @param CompletionContext $context    Completion context.
175
	 *
176
	 * @return array
177
	 */
178
	public function completeOptionValues($optionName, CompletionContext $context)
179
	{
180
		$ret = parent::completeOptionValues($optionName, $context);
181
182
		if ( $optionName === 'refs' ) {
183
			return $this->getAllRefs();
184
		}
185
186
		return $ret;
187
	}
188
189
	/**
0 ignored issues
show
introduced by
Doc comment for parameter "$input" missing
Loading history...
introduced by
Doc comment for parameter "$output" missing
Loading history...
190
	 * {@inheritdoc}
191
	 */
192
	public function initialize(InputInterface $input, OutputInterface $output)
193
	{
194
		parent::initialize($input, $output);
195
196
		$this->_revisionLog = $this->getRevisionLog($this->getWorkingCopyUrl());
197
	}
198
199
	/**
200
	 * {@inheritdoc}
0 ignored issues
show
introduced by
Doc comment short description must start with a capital letter
Loading history...
201
	 */
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...
202
	protected function execute(InputInterface $input, OutputInterface $output)
203
	{
204
		$bugs = $this->getList($this->io->getOption('bugs'));
205
		$revisions = $this->getList($this->io->getOption('revisions'));
206
207
		if ( $bugs && $revisions ) {
208
			throw new \RuntimeException('The "--bugs" and "--revisions" options are mutually exclusive.');
209
		}
210
211
		$missing_revisions = array();
212
		$revisions_by_path = $this->getRevisionsByPath();
213
214
		if ( $revisions ) {
215
			$revisions = $this->_revisionListParser->expandRanges($revisions);
216
			$revisions_by_path = array_intersect($revisions_by_path, $revisions);
217
			$missing_revisions = array_diff($revisions, $revisions_by_path);
218
		}
219
		elseif ( $bugs ) {
220
			// Only show bug-related revisions on given path. The $missing_revisions is always empty.
221
			$revisions_from_bugs = $this->_revisionLog->find('bugs', $bugs);
222
			$revisions_by_path = array_intersect($revisions_by_path, $revisions_from_bugs);
223
		}
224
225
		$merged_by = $this->getList($this->io->getOption('merged-by'));
226
227
		if ( $merged_by ) {
228
			// Exclude revisions, that were merged outside of project root folder in repository.
229
			$merged_by = $this->_revisionListParser->expandRanges($merged_by);
230
			$revisions_by_path = $this->_revisionLog->find(
231
				'paths',
232
				$this->repositoryConnector->getProjectUrl(
233
					$this->repositoryConnector->getRelativePath($this->getWorkingCopyPath())
234
				)
235
			);
236
			$revisions_by_path = array_intersect($revisions_by_path, $this->_revisionLog->find('merges', $merged_by));
237
		}
238
239
		if ( $this->io->getOption('merges') ) {
240
			$revisions_by_path = array_intersect($revisions_by_path, $this->_revisionLog->find('merges', 'all_merges'));
241
		}
242
		elseif ( $this->io->getOption('no-merges') ) {
243
			$revisions_by_path = array_diff($revisions_by_path, $this->_revisionLog->find('merges', 'all_merges'));
244
		}
245
246
		if ( $this->io->getOption('merged') ) {
247
			$revisions_by_path = array_intersect($revisions_by_path, $this->_revisionLog->find('merges', 'all_merged'));
248
		}
249
		elseif ( $this->io->getOption('not-merged') ) {
250
			$revisions_by_path = array_diff($revisions_by_path, $this->_revisionLog->find('merges', 'all_merged'));
251
		}
252
253
		if ( $missing_revisions ) {
254
			throw new CommandException($this->getMissingRevisionsErrorMessage($missing_revisions));
255
		}
256
		elseif ( !$revisions_by_path ) {
257
			throw new CommandException('No matching revisions found.');
258
		}
259
260
		rsort($revisions_by_path, SORT_NUMERIC);
261
262
		if ( $bugs || $revisions ) {
263
			// Don't limit revisions, when provided explicitly by user.
264
			$revisions_by_path_with_limit = $revisions_by_path;
265
		}
266
		else {
267
			// Apply limit only, when no explicit bugs/revisions are set.
268
			$revisions_by_path_with_limit = array_slice($revisions_by_path, 0, $this->getMaxCount());
269
		}
270
271
		$revisions_by_path_count = count($revisions_by_path);
272
		$revisions_by_path_with_limit_count = count($revisions_by_path_with_limit);
273
274
		$this->io->writeln(sprintf(
275
			' * Showing <info>%d</info> of <info>%d</info> revision(-s):',
276
			$revisions_by_path_with_limit_count,
277
			$revisions_by_path_count
278
		));
279
280
		$this->printRevisions($revisions_by_path_with_limit, (boolean)$this->io->getOption('details'));
281
	}
282
283
	/**
284
	 * Shows error about missing revisions.
285
	 *
286
	 * @param array $missing_revisions Missing revisions.
287
	 *
288
	 * @return string
289
	 */
290
	protected function getMissingRevisionsErrorMessage(array $missing_revisions)
291
	{
292
		$refs = $this->io->getOption('refs');
293
		$missing_revisions = implode(', ', $missing_revisions);
294
295
		if ( $refs ) {
296
			$revision_source = 'in "' . $refs . '" ref(-s)';
297
		}
298
		else {
299
			$revision_source = 'at "' . $this->getWorkingCopyUrl() . '" url';
300
		}
301
302
		return 'The ' . $missing_revisions . ' revision(-s) not found ' . $revision_source . '.';
303
	}
304
305
	/**
306
	 * Returns list of revisions by path.
307
	 *
308
	 * @return array
309
	 * @throws CommandException When given refs doesn't exist.
310
	 */
311
	protected function getRevisionsByPath()
312
	{
313
		$refs = $this->getList($this->io->getOption('refs'));
314
		$relative_path = $this->repositoryConnector->getRelativePath($this->getWorkingCopyPath());
315
316
		if ( !$refs ) {
317
			$ref = $this->repositoryConnector->getRefByPath($relative_path);
318
319
			// Use search by ref, when working copy represents ref root folder.
320
			if ( $ref !== false && preg_match('/' . preg_quote($ref, '/') . '$/', $relative_path) ) {
321
				return $this->_revisionLog->find('refs', $ref);
322
			}
323
		}
324
325
		if ( $refs ) {
326
			$incorrect_refs = array_diff($refs, $this->getAllRefs());
327
328
			if ( $incorrect_refs ) {
329
				throw new CommandException(
330
					'The following refs are unknown: "' . implode('", "', $incorrect_refs) . '".'
331
				);
332
			}
333
334
			return $this->_revisionLog->find('refs', $refs);
335
		}
336
337
		return $this->_revisionLog->find('paths', $relative_path);
338
	}
339
340
	/**
341
	 * Returns displayed revision limit.
342
	 *
343
	 * @return integer
344
	 */
345
	protected function getMaxCount()
346
	{
347
		$max_count = $this->io->getOption('max-count');
348
349
		if ( $max_count !== null ) {
350
			return $max_count;
351
		}
352
353
		return $this->getSetting(self::SETTING_LOG_LIMIT);
354
	}
355
356
	/**
357
	 * Prints revisions.
358
	 *
359
	 * @param array   $revisions    Revisions.
360
	 * @param boolean $with_details Print extended revision details (e.g. paths changed).
361
	 *
362
	 * @return void
363
	 */
364
	protected function printRevisions(array $revisions, $with_details = false)
365
	{
366
		$table = new Table($this->io->getOutput());
367
		$headers = array('Revision', 'Author', 'Date', 'Bug-ID', 'Log Message');
368
369
		// Add "Summary" header.
370
		if ( $this->io->getOption('summary') ) {
371
			$headers[] = 'Summary';
372
		}
373
374
		$merge_oracle = $this->io->getOption('merge-oracle');
375
376
		// Add "M.O." header.
377
		if ( $merge_oracle ) {
378
			$headers[] = 'M.O.';
379
			$merge_conflict_regexps = $this->getMergeConflictRegExps();
380
		}
381
382
		// Add "Merged Via" header.
383
		$merge_status = $this->io->getOption('merge-status');
384
385
		if ( $merge_status ) {
386
			$headers[] = 'Merged Via';
387
		}
388
389
		$table->setHeaders($headers);
390
391
		/** @var DateHelper $date_helper */
392
		$date_helper = $this->getHelper('date');
393
394
		$prev_bugs = null;
395
		$last_color = 'yellow';
396
		$last_revision = end($revisions);
397
398
		$repository_path = $this->repositoryConnector->getRelativePath(
399
			$this->getWorkingCopyPath()
400
		) . '/';
401
402
		foreach ( $revisions as $revision ) {
403
			$revision_data = $this->_revisionLog->getRevisionData('summary', $revision);
404
			list($log_message,) = explode(PHP_EOL, $revision_data['msg']);
405
			$log_message = preg_replace('/^\[fixes:.*?\]/', "\xE2\x9C\x94", $log_message);
406
407
			if ( mb_strlen($log_message) > 68 ) {
408
				$log_message = mb_substr($log_message, 0, 68 - 3) . '...';
409
			}
410
411
			$new_bugs = implode(', ', $this->_revisionLog->getRevisionData('bugs', $revision));
412
413
			if ( isset($prev_bugs) && $new_bugs <> $prev_bugs ) {
414
				$last_color = $last_color == 'yellow' ? 'magenta' : 'yellow';
415
			}
416
417
			$row = array(
418
				$revision,
419
				$revision_data['author'],
420
				$date_helper->getAgoTime($revision_data['date']),
421
				'<fg=' . $last_color . '>' . $new_bugs . '</>',
422
				$log_message,
423
			);
424
425
			$revision_paths = $this->_revisionLog->getRevisionData('paths', $revision);
426
427
			// Add "Summary" column.
428
			if ( $this->io->getOption('summary') ) {
429
				$row[] = $this->generateChangeSummary($revision_paths);
430
			}
431
432
			// Add "M.O." column.
433
			if ( $merge_oracle ) {
434
				$merge_conflict_predication = $this->getMergeConflictPrediction(
435
					$revision_paths,
436
					$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...
437
				);
438
				$row[] = $merge_conflict_predication ? '<error>' . count($merge_conflict_predication) . '</error>' : '';
439
			}
440
			else {
441
				$merge_conflict_predication = array();
442
			}
443
444
			// Add "Merged Via" column.
445
			if ( $merge_status ) {
446
				$merged_via = $this->_revisionLog->getRevisionData('merges', $revision);
447
				$row[] = $merged_via ? implode(', ', $merged_via) : '';
448
			}
449
450
			$table->addRow($row);
451
452
			if ( $with_details ) {
453
				$details = '<fg=white;options=bold>Changed Paths:</>';
454
455
				foreach ( $revision_paths as $path_data ) {
456
					$path_action = $path_data['action'];
457
					$relative_path = $this->_getRelativeLogPath($path_data, 'path', $repository_path);
458
459
					$details .= PHP_EOL . ' * ';
460
461
					if ( $path_action == 'A' ) {
462
						$color_format = 'fg=green';
463
					}
464
					elseif ( $path_action == 'D' ) {
465
						$color_format = 'fg=red';
466
					}
467
					else {
468
						$color_format = in_array($path_data['path'], $merge_conflict_predication) ? 'error' : '';
469
					}
470
471
					$to_colorize = $path_action . '    ' . $relative_path;
472
473
					if ( isset($path_data['copyfrom-path']) ) {
474
						$copy_from_rev = $path_data['copyfrom-rev'];
475
						$copy_from_path = $this->_getRelativeLogPath($path_data, 'copyfrom-path', $repository_path);
476
						$to_colorize .= PHP_EOL . '        (from ' . $copy_from_path . ':' . $copy_from_rev . ')';
477
					}
478
479
					if ( $color_format ) {
480
						$to_colorize = '<' . $color_format . '>' . $to_colorize . '</>';
481
					}
482
483
					$details .= $to_colorize;
484
				}
485
486
				$table->addRow(new TableSeparator());
487
				$table->addRow(array(new TableCell($details, array('colspan' => 5))));
488
489
				if ( $revision != $last_revision ) {
490
					$table->addRow(new TableSeparator());
491
				}
492
			}
493
494
			$prev_bugs = $new_bugs;
495
		}
496
497
		$table->render();
498
	}
499
500
	/**
501
	 * Generates change summary for a revision.
502
	 *
503
	 * @param array $revision_paths Revision paths.
504
	 *
505
	 * @return string
506
	 */
507
	protected function generateChangeSummary(array $revision_paths)
508
	{
509
		$summary = array('added' => 0, 'changed' => 0, 'removed' => 0);
510
511
		foreach ( $revision_paths as $path_data ) {
512
			$path_action = $path_data['action'];
513
514
			if ( $path_action == 'A' ) {
515
				$summary['added']++;
516
			}
517
			elseif ( $path_action == 'D' ) {
518
				$summary['removed']++;
519
			}
520
			else {
521
				$summary['changed']++;
522
			}
523
		}
524
525
		if ( $summary['added'] ) {
526
			$summary['added'] = '<fg=green>+' . $summary['added'] . '</>';
527
		}
528
529
		if ( $summary['removed'] ) {
530
			$summary['removed'] = '<fg=red>-' . $summary['removed'] . '</>';
531
		}
532
533
		return implode(' ', array_filter($summary));
534
	}
535
536
	/**
537
	 * Returns merge conflict path predictions.
538
	 *
539
	 * @param array $revision_paths         Revision paths.
540
	 * @param array $merge_conflict_regexps Merge conflict paths.
541
	 *
542
	 * @return array
543
	 */
544
	protected function getMergeConflictPrediction(array $revision_paths, array $merge_conflict_regexps)
545
	{
546
		if ( !$merge_conflict_regexps ) {
547
			return array();
548
		}
549
550
		$conflict_paths = array();
551
552
		foreach ( $revision_paths as $revision_path ) {
553
			foreach ( $merge_conflict_regexps as $merge_conflict_regexp ) {
554
				if ( preg_match($merge_conflict_regexp, $revision_path['path']) ) {
555
					$conflict_paths[] = $revision_path['path'];
556
				}
557
			}
558
		}
559
560
		return $conflict_paths;
561
	}
562
563
	/**
564
	 * Returns merge conflict regexps.
565
	 *
566
	 * @return array
567
	 */
568
	protected function getMergeConflictRegExps()
569
	{
570
		return $this->getSetting(self::SETTING_LOG_MERGE_CONFLICT_REGEXPS);
571
	}
572
573
	/**
574
	 * Returns relative path to "svn log" returned path.
575
	 *
576
	 * @param array  $path_data       Path data.
577
	 * @param string $path_key        Path key.
578
	 * @param string $repository_path Repository path.
579
	 *
580
	 * @return string
581
	 */
582
	private function _getRelativeLogPath(array $path_data, $path_key, $repository_path)
583
	{
584
		$ret = $path_data[$path_key];
585
586
		if ( $path_data['kind'] == 'dir' ) {
587
			$ret .= '/';
588
		}
589
590
		$ret = preg_replace('/^' . preg_quote($repository_path, '/') . '/', '', $ret, 1);
591
592
		if ( $ret === '' ) {
593
			$ret = '.';
594
		}
595
596
		return $ret;
597
	}
598
599
	/**
600
	 * Returns URL to the working copy.
601
	 *
602
	 * @return string
603
	 */
604
	protected function getWorkingCopyUrl()
605
	{
606
		$wc_path = $this->getWorkingCopyPath();
607
608
		if ( !$this->repositoryConnector->isUrl($wc_path) ) {
609
			return $this->repositoryConnector->getWorkingCopyUrl($wc_path);
610
		}
611
612
		return $wc_path;
613
	}
614
615
	/**
616
	 * Returns list of config settings.
617
	 *
618
	 * @return AbstractConfigSetting[]
619
	 */
620
	public function getConfigSettings()
621
	{
622
		return array(
623
			new IntegerConfigSetting(self::SETTING_LOG_LIMIT, 10),
624
			new RegExpsConfigSetting(self::SETTING_LOG_MERGE_CONFLICT_REGEXPS, '#/composer\.lock$#'),
625
		);
626
	}
627
628
}
629