Failed Conditions
Push — master ( 51062b...e58a5b )
by Alexander
03:27
created

LogCommand::initialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 6
ccs 0
cts 4
cp 0
rs 9.4285
cc 1
eloc 3
nc 1
nop 2
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\Repository\Parser\RevisionListParser;
19
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\RevisionLog;
20
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\RevisionPrinter;
21
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
22
use Symfony\Component\Console\Input\InputArgument;
23
use Symfony\Component\Console\Input\InputInterface;
24
use Symfony\Component\Console\Input\InputOption;
25
use Symfony\Component\Console\Output\OutputInterface;
26
27
class LogCommand extends AbstractCommand implements IAggregatorAwareCommand, IConfigAwareCommand
28
{
29
30
	const SETTING_LOG_LIMIT = 'log.limit';
31
32
	const SETTING_LOG_MESSAGE_LIMIT = 'log.message-limit';
33
34
	const SETTING_LOG_MERGE_CONFLICT_REGEXPS = 'log.merge-conflict-regexps';
35
36
	/**
37
	 * Revision list parser.
38
	 *
39
	 * @var RevisionListParser
40
	 */
41
	private $_revisionListParser;
42
43
	/**
44
	 * Revision log
45
	 *
46
	 * @var RevisionLog
47
	 */
48
	private $_revisionLog;
49
50
	/**
51
	 * Revision printer.
52
	 *
53
	 * @var RevisionPrinter
54
	 */
55
	private $_revisionPrinter;
56
57
	/**
58
	 * Prepare dependencies.
59
	 *
60
	 * @return void
61
	 */
62
	protected function prepareDependencies()
63
	{
64
		parent::prepareDependencies();
65
66
		$container = $this->getContainer();
67
68
		$this->_revisionListParser = $container['revision_list_parser'];
69
		$this->_revisionPrinter = $container['revision_printer'];
70
	}
71
72
	/**
73
	 * {@inheritdoc}
74
	 */
75
	protected function configure()
76
	{
77
		$this->pathAcceptsUrl = true;
78
79
		$this
80
			->setName('log')
81
			->setDescription(
82
				'Show the log messages for a set of revisions, bugs, paths, refs, etc.'
83
			)
84
			->addArgument(
85
				'path',
86
				InputArgument::OPTIONAL,
87
				'Working copy path or URL',
88
				'.'
89
			)
90
			->addOption(
91
				'revisions',
92
				'r',
93
				InputOption::VALUE_REQUIRED,
94
				'List of revision(-s) and/or revision range(-s), e.g. <comment>53324</comment>, <comment>1224-4433</comment>'
95
			)
96
			->addOption(
97
				'bugs',
98
				'b',
99
				InputOption::VALUE_REQUIRED,
100
				'List of bug(-s), e.g. <comment>JRA-1234</comment>, <comment>43644</comment>'
101
			)
102
			->addOption(
103
				'refs',
104
				null,
105
				InputOption::VALUE_REQUIRED,
106
				'List of refs, e.g. <comment>trunk</comment>, <comment>branches/branch-name</comment>, <comment>tags/tag-name</comment>'
107
			)
108
			->addOption(
109
				'merges',
110
				null,
111
				InputOption::VALUE_NONE,
112
				'Show merge revisions only'
113
			)
114
			->addOption(
115
				'no-merges',
116
				null,
117
				InputOption::VALUE_NONE,
118
				'Hide merge revisions'
119
			)
120
			->addOption(
121
				'merged',
122
				null,
123
				InputOption::VALUE_NONE,
124
				'Shows only revisions, that were merged at least once'
125
			)
126
			->addOption(
127
				'not-merged',
128
				null,
129
				InputOption::VALUE_NONE,
130
				'Shows only revisions, that were not merged'
131
			)
132
			->addOption(
133
				'merged-by',
134
				null,
135
				InputOption::VALUE_REQUIRED,
136
				'Show revisions merged by list of revision(-s) and/or revision range(-s)'
137
			)
138
			->addOption(
139
				'action',
140
				null,
141
				InputOption::VALUE_REQUIRED,
142
				'Show revisions, whose paths were affected by specified action, e.g. <comment>A</comment>, <comment>M</comment>, <comment>R</comment>, <comment>D</comment>'
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 140 characters; contains 172 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
143
			)
144
			->addOption(
145
				'kind',
146
				null,
147
				InputOption::VALUE_REQUIRED,
148
				'Show revisions, whose paths match specified kind, e.g. <comment>dir</comment> or <comment>file</comment>'
149
			)
150
			->addOption(
151
				'with-details',
152
				'd',
153
				InputOption::VALUE_NONE,
154
				'Shows detailed revision information, e.g. paths affected'
155
			)
156
			->addOption(
157
				'with-summary',
158
				's',
159
				InputOption::VALUE_NONE,
160
				'Shows number of added/changed/removed paths in the revision'
161
			)
162
			->addOption(
163
				'with-refs',
164
				null,
165
				InputOption::VALUE_NONE,
166
				'Shows revision refs'
167
			)
168
			->addOption(
169
				'with-merge-oracle',
170
				null,
171
				InputOption::VALUE_NONE,
172
				'Shows number of paths in the revision, that can cause conflict upon merging'
173
			)
174
			->addOption(
175
				'with-merge-status',
176
				null,
177
				InputOption::VALUE_NONE,
178
				'Shows merge revisions affecting this revision'
179
			)
180
			->addOption(
181
				'max-count',
182
				null,
183
				InputOption::VALUE_REQUIRED,
184
				'Limit the number of revisions to output'
185
			);
186
187
		parent::configure();
188
	}
189
190
	/**
191
	 * Return possible values for the named option
192
	 *
193
	 * @param string            $optionName Option name.
194
	 * @param CompletionContext $context    Completion context.
195
	 *
196
	 * @return array
197
	 */
198
	public function completeOptionValues($optionName, CompletionContext $context)
199
	{
200
		$ret = parent::completeOptionValues($optionName, $context);
201
202
		if ( $optionName === 'refs' ) {
203
			return $this->getAllRefs();
204
		}
205
		elseif ( $optionName === 'action' ) {
206
			return $this->getAllActions();
207
		}
208
		elseif ( $optionName === 'kind' ) {
209
			return $this->getAllKinds();
210
		}
211
212
		return $ret;
213
	}
214
215
	/**
216
	 * {@inheritdoc}
217
	 */
218
	public function initialize(InputInterface $input, OutputInterface $output)
219
	{
220
		parent::initialize($input, $output);
221
222
		$this->_revisionLog = $this->getRevisionLog($this->getWorkingCopyUrl());
223
	}
224
225
	/**
226
	 * {@inheritdoc}
227
	 *
228
	 * @throws \RuntimeException When both "--bugs" and "--revisions" options were specified.
229
	 * @throws CommandException When specified revisions are not present in current project.
230
	 * @throws CommandException When project contains no associated revisions.
231
	 */
232
	protected function execute(InputInterface $input, OutputInterface $output)
233
	{
234
		$bugs = $this->getList($this->io->getOption('bugs'));
235
		$revisions = $this->getList($this->io->getOption('revisions'));
236
237
		if ( $bugs && $revisions ) {
238
			throw new \RuntimeException('The "--bugs" and "--revisions" options are mutually exclusive.');
239
		}
240
241
		$missing_revisions = array();
242
		$revisions_by_path = $this->getRevisionsByPath();
243
244
		if ( $revisions ) {
245
			$revisions = $this->_revisionListParser->expandRanges($revisions);
246
			$revisions_by_path = array_intersect($revisions_by_path, $revisions);
247
			$missing_revisions = array_diff($revisions, $revisions_by_path);
248
		}
249
		elseif ( $bugs ) {
250
			// Only show bug-related revisions on given path. The $missing_revisions is always empty.
251
			$revisions_from_bugs = $this->_revisionLog->find('bugs', $bugs);
252
			$revisions_by_path = array_intersect($revisions_by_path, $revisions_from_bugs);
253
		}
254
255
		$merged_by = $this->getList($this->io->getOption('merged-by'));
256
257
		if ( $merged_by ) {
258
			$merged_by = $this->_revisionListParser->expandRanges($merged_by);
259
			$revisions_by_path = $this->_revisionLog->find('merges', $merged_by);
260
		}
261
262
		if ( $this->io->getOption('merges') ) {
263
			$revisions_by_path = array_intersect($revisions_by_path, $this->_revisionLog->find('merges', 'all_merges'));
264
		}
265
		elseif ( $this->io->getOption('no-merges') ) {
266
			$revisions_by_path = array_diff($revisions_by_path, $this->_revisionLog->find('merges', 'all_merges'));
267
		}
268
269
		if ( $this->io->getOption('merged') ) {
270
			$revisions_by_path = array_intersect($revisions_by_path, $this->_revisionLog->find('merges', 'all_merged'));
271
		}
272
		elseif ( $this->io->getOption('not-merged') ) {
273
			$revisions_by_path = array_diff($revisions_by_path, $this->_revisionLog->find('merges', 'all_merged'));
274
		}
275
276
		$action = $this->io->getOption('action');
277
278
		if ( $action ) {
279
			if ( !in_array($action, $this->getAllActions()) ) {
280
				throw new CommandException('The "' . $action . '" action is unknown.');
281
			}
282
283
			$revisions_by_path = array_intersect(
284
				$revisions_by_path,
285
				$this->_revisionLog->find('paths', 'action:' . $action)
286
			);
287
		}
288
289
		$kind = $this->io->getOption('kind');
290
291
		if ( $kind ) {
292
			if ( !in_array($kind, $this->getAllKinds()) ) {
293
				throw new CommandException('The "' . $kind . '" kind is unknown.');
294
			}
295
296
			$revisions_by_path = array_intersect(
297
				$revisions_by_path,
298
				$this->_revisionLog->find('paths', 'kind:' . $kind)
299
			);
300
		}
301
302
		if ( $missing_revisions ) {
303
			throw new CommandException($this->getMissingRevisionsErrorMessage($missing_revisions));
304
		}
305
		elseif ( !$revisions_by_path ) {
306
			throw new CommandException('No matching revisions found.');
307
		}
308
309
		rsort($revisions_by_path, SORT_NUMERIC);
310
311
		if ( $bugs || $revisions ) {
312
			// Don't limit revisions, when provided explicitly by user.
313
			$revisions_by_path_with_limit = $revisions_by_path;
314
		}
315
		else {
316
			// Apply limit only, when no explicit bugs/revisions are set.
317
			$revisions_by_path_with_limit = array_slice($revisions_by_path, 0, $this->getMaxCount());
318
		}
319
320
		$revisions_by_path_count = count($revisions_by_path);
321
		$revisions_by_path_with_limit_count = count($revisions_by_path_with_limit);
322
323
		if ( $revisions_by_path_with_limit_count === $revisions_by_path_count ) {
324
			$this->io->writeln(sprintf(
325
				' * Showing <info>%d</info> revision(-s) in %s:',
326
				$revisions_by_path_with_limit_count,
327
				$this->getRevisionLogIdentifier()
328
			));
329
		}
330
		else {
331
			$this->io->writeln(sprintf(
332
				' * Showing <info>%d</info> of <info>%d</info> revision(-s) in %s:',
333
				$revisions_by_path_with_limit_count,
334
				$revisions_by_path_count,
335
				$this->getRevisionLogIdentifier()
336
			));
337
		}
338
339
		$this->printRevisions($revisions_by_path_with_limit);
340
	}
341
342
	/**
343
	 * Returns all actions.
344
	 *
345
	 * @return array
346
	 */
347
	protected function getAllActions()
348
	{
349
		return array('A', 'M', 'R', 'D');
350
	}
351
352
	/**
353
	 * Returns all actions.
354
	 *
355
	 * @return array
356
	 */
357
	protected function getAllKinds()
358
	{
359
		return array('dir', 'file');
360
	}
361
362
	/**
363
	 * Returns revision log identifier.
364
	 *
365
	 * @return string
366
	 */
367
	protected function getRevisionLogIdentifier()
368
	{
369
		$ret = '<info>' . $this->_revisionLog->getProjectPath() . '</info> project';
370
371
		$ref_name = $this->_revisionLog->getRefName();
372
373
		if ( $ref_name ) {
374
			$ret .= ' (ref: <info>' . $ref_name . '</info>)';
375
		}
376
		else {
377
			$ret .= ' (all refs)';
378
		}
379
380
		return $ret;
381
	}
382
383
	/**
384
	 * Shows error about missing revisions.
385
	 *
386
	 * @param array $missing_revisions Missing revisions.
387
	 *
388
	 * @return string
389
	 */
390
	protected function getMissingRevisionsErrorMessage(array $missing_revisions)
391
	{
392
		$refs = $this->io->getOption('refs');
393
		$missing_revisions = implode(', ', $missing_revisions);
394
395
		if ( $refs ) {
396
			$revision_source = 'in "' . $refs . '" ref(-s)';
397
		}
398
		else {
399
			$revision_source = 'at "' . $this->getWorkingCopyUrl() . '" url';
400
		}
401
402
		return 'The ' . $missing_revisions . ' revision(-s) not found ' . $revision_source . '.';
403
	}
404
405
	/**
406
	 * Returns list of revisions by path.
407
	 *
408
	 * @return array
409
	 * @throws CommandException When given refs doesn't exist.
410
	 */
411
	protected function getRevisionsByPath()
412
	{
413
		$path = $this->io->getArgument('path');
414
		$wc_path = $this->getWorkingCopyPath(); // When "$path" represents deleted path will be parent folder.
415
416
		$refs = $this->getList($this->io->getOption('refs'));
417
		$relative_path = $this->repositoryConnector->getRelativePath($wc_path);
418
419
		if ( !$this->repositoryConnector->isUrl($wc_path) ) {
420
			$relative_path .= $this->_getPathDifference($wc_path, $path);
421
		}
422
423
		if ( file_exists($wc_path) ) {
424
			// This is existing directory - show history recursively.
425
			if ( is_dir($path) ) {
426
				$relative_path .= '/';
427
			}
428
		}
429
		else {
430
			// This is deleted path - show history recursively only,
431
			// when it doesn't contain extension (maybe a folder).
432
			if ( !pathinfo($path, PATHINFO_EXTENSION) ) {
433
				$relative_path .= '/';
434
			}
435
		}
436
437
		if ( !$refs ) {
438
			$ref = $this->repositoryConnector->getRefByPath($relative_path);
439
440
			// Use search by ref, when working copy represents ref root folder.
441
			if ( $ref !== false && preg_match('#' . preg_quote($ref, '#') . '/$#', $relative_path) ) {
442
				return $this->_revisionLog->find('refs', $ref);
443
			}
444
		}
445
446
		if ( $refs ) {
447
			$incorrect_refs = array_diff($refs, $this->getAllRefs());
448
449
			if ( $incorrect_refs ) {
450
				throw new CommandException(
451
					'The following refs are unknown: "' . implode('", "', $incorrect_refs) . '".'
452
				);
453
			}
454
455
			return $this->_revisionLog->find('refs', $refs);
456
		}
457
458
		return $this->_revisionLog->find('paths', $relative_path);
459
	}
460
461
	/**
462
	 * Returns difference between 2 paths.
463
	 *
464
	 * @param string $main_path Main path.
465
	 * @param string $sub_path  Sub path.
466
	 *
467
	 * @return string
468
	 */
469
	private function _getPathDifference($main_path, $sub_path)
470
	{
471
		if ( strpos($sub_path, '.') !== false ) {
472
			$sub_path = realpath($sub_path);
473
		}
474
475
		$adapted_sub_path = $sub_path;
476
477
		do {
478
			$sub_path_pos = strpos($main_path, $adapted_sub_path);
479
480
			if ( $sub_path_pos !== false ) {
481
				break;
482
			}
483
484
			$adapted_sub_path = dirname($adapted_sub_path);
485
		} while ( strlen($adapted_sub_path) );
0 ignored issues
show
Coding Style Performance introduced by
The use of strlen() inside a loop condition is not allowed; assign the return value to a variable and use the variable in the loop condition instead
Loading history...
486
487
		// No sub-matches.
488
		if ( !strlen($adapted_sub_path) ) {
489
			return '';
490
		}
491
492
		return str_replace($adapted_sub_path, '', $sub_path);
493
	}
494
495
	/**
496
	 * Returns displayed revision limit.
497
	 *
498
	 * @return integer
499
	 */
500
	protected function getMaxCount()
501
	{
502
		$max_count = $this->io->getOption('max-count');
503
504
		if ( $max_count !== null ) {
505
			return $max_count;
506
		}
507
508
		return $this->getSetting(self::SETTING_LOG_LIMIT);
509
	}
510
511
	/**
512
	 * Prints revisions.
513
	 *
514
	 * @param array $revisions Revisions.
515
	 *
516
	 * @return void
517
	 */
518
	protected function printRevisions(array $revisions)
519
	{
520
		$column_mapping = array(
521
			'with-details' => RevisionPrinter::COLUMN_DETAILS,
522
			'with-summary' => RevisionPrinter::COLUMN_SUMMARY,
523
			'with-refs' => RevisionPrinter::COLUMN_REFS,
524
			'with-merge-oracle' => RevisionPrinter::COLUMN_MERGE_ORACLE,
525
			'with-merge-status' => RevisionPrinter::COLUMN_MERGE_STATUS,
526
		);
527
528
		foreach ( $column_mapping as $option_name => $column ) {
529
			if ( $this->io->getOption($option_name) ) {
530
				$this->_revisionPrinter->withColumn($column);
531
			}
532
		}
533
534
		$this->_revisionPrinter->setMergeConflictRegExps($this->getSetting(self::SETTING_LOG_MERGE_CONFLICT_REGEXPS));
535
		$this->_revisionPrinter->setLogMessageLimit($this->getSetting(self::SETTING_LOG_MESSAGE_LIMIT));
536
537
		$this->_revisionPrinter->printRevisions($this->_revisionLog, $revisions, $this->io->getOutput());
538
	}
539
540
	/**
541
	 * Returns list of config settings.
542
	 *
543
	 * @return AbstractConfigSetting[]
544
	 */
545
	public function getConfigSettings()
546
	{
547
		return array(
548
			new IntegerConfigSetting(self::SETTING_LOG_LIMIT, 10),
549
			new IntegerConfigSetting(self::SETTING_LOG_MESSAGE_LIMIT, 68),
550
			new RegExpsConfigSetting(self::SETTING_LOG_MERGE_CONFLICT_REGEXPS, '#/composer\.lock$#'),
551
		);
552
	}
553
554
}
555