Failed Conditions
Push — master ( 1523fb...3c2402 )
by Alexander
03:05
created

LogCommand::getAllKinds()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
cc 1
eloc 2
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\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. <info>A</info>, <info>M</info>, <info>R</info>, <info>D</info>'
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 140 characters; contains 148 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. <info>dir</info> or <info>file</info>'
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
		$refs = $this->getList($this->io->getOption('refs'));
414
		$relative_path = $this->repositoryConnector->getRelativePath($this->getWorkingCopyPath()) . '/';
415
416
		if ( !$refs ) {
417
			$ref = $this->repositoryConnector->getRefByPath($relative_path);
418
419
			// Use search by ref, when working copy represents ref root folder.
420
			if ( $ref !== false && preg_match('#' . preg_quote($ref, '#') . '/$#', $relative_path) ) {
421
				return $this->_revisionLog->find('refs', $ref);
422
			}
423
		}
424
425
		if ( $refs ) {
426
			$incorrect_refs = array_diff($refs, $this->getAllRefs());
427
428
			if ( $incorrect_refs ) {
429
				throw new CommandException(
430
					'The following refs are unknown: "' . implode('", "', $incorrect_refs) . '".'
431
				);
432
			}
433
434
			return $this->_revisionLog->find('refs', $refs);
435
		}
436
437
		return $this->_revisionLog->find('paths', $relative_path);
438
	}
439
440
	/**
441
	 * Returns displayed revision limit.
442
	 *
443
	 * @return integer
444
	 */
445
	protected function getMaxCount()
446
	{
447
		$max_count = $this->io->getOption('max-count');
448
449
		if ( $max_count !== null ) {
450
			return $max_count;
451
		}
452
453
		return $this->getSetting(self::SETTING_LOG_LIMIT);
454
	}
455
456
	/**
457
	 * Prints revisions.
458
	 *
459
	 * @param array $revisions Revisions.
460
	 *
461
	 * @return void
462
	 */
463
	protected function printRevisions(array $revisions)
464
	{
465
		$column_mapping = array(
466
			'with-details' => RevisionPrinter::COLUMN_DETAILS,
467
			'with-summary' => RevisionPrinter::COLUMN_SUMMARY,
468
			'with-refs' => RevisionPrinter::COLUMN_REFS,
469
			'with-merge-oracle' => RevisionPrinter::COLUMN_MERGE_ORACLE,
470
			'with-merge-status' => RevisionPrinter::COLUMN_MERGE_STATUS,
471
		);
472
473
		foreach ( $column_mapping as $option_name => $column ) {
474
			if ( $this->io->getOption($option_name) ) {
475
				$this->_revisionPrinter->withColumn($column);
476
			}
477
		}
478
479
		$this->_revisionPrinter->setMergeConflictRegExps($this->getSetting(self::SETTING_LOG_MERGE_CONFLICT_REGEXPS));
480
		$this->_revisionPrinter->setLogMessageLimit($this->getSetting(self::SETTING_LOG_MESSAGE_LIMIT));
481
482
		$this->_revisionPrinter->printRevisions($this->_revisionLog, $revisions, $this->io->getOutput());
483
	}
484
485
	/**
486
	 * Returns list of config settings.
487
	 *
488
	 * @return AbstractConfigSetting[]
489
	 */
490
	public function getConfigSettings()
491
	{
492
		return array(
493
			new IntegerConfigSetting(self::SETTING_LOG_LIMIT, 10),
494
			new IntegerConfigSetting(self::SETTING_LOG_MESSAGE_LIMIT, 68),
495
			new RegExpsConfigSetting(self::SETTING_LOG_MERGE_CONFLICT_REGEXPS, '#/composer\.lock$#'),
496
		);
497
	}
498
499
}
500