Failed Conditions
Push — master ( c1c7d6...c5e998 )
by Alexander
03:04
created

LogCommand::getMaxCount()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 10
ccs 0
cts 5
cp 0
rs 9.4285
cc 2
eloc 5
nc 2
nop 0
crap 6
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 ConsoleHelpers\SVNBuddy\Repository\RevisionLog\RevisionPrinter;
22
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
23
use Symfony\Component\Console\Helper\Table;
24
use Symfony\Component\Console\Helper\TableCell;
25
use Symfony\Component\Console\Helper\TableSeparator;
26
use Symfony\Component\Console\Input\InputArgument;
27
use Symfony\Component\Console\Input\InputInterface;
28
use Symfony\Component\Console\Input\InputOption;
29
use Symfony\Component\Console\Output\OutputInterface;
30
31
class LogCommand extends AbstractCommand implements IAggregatorAwareCommand, IConfigAwareCommand
32
{
33
34
	const SETTING_LOG_LIMIT = 'log.limit';
35
36
	const SETTING_LOG_MESSAGE_LIMIT = 'log.message-limit';
37
38
	const SETTING_LOG_MERGE_CONFLICT_REGEXPS = 'log.merge-conflict-regexps';
39
40
	/**
41
	 * Revision list parser.
42
	 *
43
	 * @var RevisionListParser
44
	 */
45
	private $_revisionListParser;
46
47
	/**
48
	 * Revision log
49
	 *
50
	 * @var RevisionLog
51
	 */
52
	private $_revisionLog;
53
54
	/**
55
	 * Revision printer.
56
	 *
57
	 * @var RevisionPrinter
58
	 */
59
	private $_revisionPrinter;
60
61
	/**
62
	 * Prepare dependencies.
63
	 *
64
	 * @return void
65
	 */
66
	protected function prepareDependencies()
67
	{
68
		parent::prepareDependencies();
69
70
		$container = $this->getContainer();
71
72
		$this->_revisionListParser = $container['revision_list_parser'];
73
		$this->_revisionPrinter = $container['revision_printer'];
74
	}
75
76
	/**
77
	 * {@inheritdoc}
0 ignored issues
show
introduced by
Doc comment short description must start with a capital letter
Loading history...
78
	 */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
79
	protected function configure()
80
	{
81
		$this->pathAcceptsUrl = true;
82
83
		$this
84
			->setName('log')
85
			->setDescription(
86
				'Show the log messages for a set of revisions, bugs, paths, refs, etc.'
87
			)
88
			->addArgument(
89
				'path',
90
				InputArgument::OPTIONAL,
91
				'Working copy path or URL',
92
				'.'
93
			)
94
			->addOption(
95
				'revisions',
96
				'r',
97
				InputOption::VALUE_REQUIRED,
98
				'List of revision(-s) and/or revision range(-s), e.g. <comment>53324</comment>, <comment>1224-4433</comment>'
99
			)
100
			->addOption(
101
				'bugs',
102
				'b',
103
				InputOption::VALUE_REQUIRED,
104
				'List of bug(-s), e.g. <comment>JRA-1234</comment>, <comment>43644</comment>'
105
			)
106
			->addOption(
107
				'refs',
108
				null,
109
				InputOption::VALUE_REQUIRED,
110
				'List of refs, e.g. <comment>trunk</comment>, <comment>branches/branch-name</comment>, <comment>tags/tag-name</comment>'
111
			)
112
			->addOption(
113
				'merges',
114
				null,
115
				InputOption::VALUE_NONE,
116
				'Show merge revisions only'
117
			)
118
			->addOption(
119
				'no-merges',
120
				null,
121
				InputOption::VALUE_NONE,
122
				'Hide merge revisions'
123
			)
124
			->addOption(
125
				'merged',
126
				null,
127
				InputOption::VALUE_NONE,
128
				'Shows only revisions, that were merged at least once'
129
			)
130
			->addOption(
131
				'not-merged',
132
				null,
133
				InputOption::VALUE_NONE,
134
				'Shows only revisions, that were not merged'
135
			)
136
			->addOption(
137
				'merged-by',
138
				null,
139
				InputOption::VALUE_REQUIRED,
140
				'Show revisions merged by list of revision(-s) and/or revision range(-s)'
141
			)
142
			->addOption(
143
				'with-details',
144
				'd',
145
				InputOption::VALUE_NONE,
146
				'Shows detailed revision information, e.g. paths affected'
147
			)
148
			->addOption(
149
				'with-summary',
150
				's',
151
				InputOption::VALUE_NONE,
152
				'Shows number of added/changed/removed paths in the revision'
153
			)
154
			->addOption(
155
				'with-refs',
156
				null,
157
				InputOption::VALUE_NONE,
158
				'Shows revision refs'
159
			)
160
			->addOption(
161
				'with-merge-oracle',
162
				null,
163
				InputOption::VALUE_NONE,
164
				'Shows number of paths in the revision, that can cause conflict upon merging'
165
			)
166
			->addOption(
167
				'with-merge-status',
168
				null,
169
				InputOption::VALUE_NONE,
170
				'Shows merge revisions affecting this revision'
171
			)
172
			->addOption(
173
				'max-count',
174
				null,
175
				InputOption::VALUE_REQUIRED,
176
				'Limit the number of revisions to output'
177
			);
178
179
		parent::configure();
180
	}
181
182
	/**
183
	 * Return possible values for the named option
184
	 *
185
	 * @param string            $optionName Option name.
186
	 * @param CompletionContext $context    Completion context.
187
	 *
188
	 * @return array
189
	 */
190
	public function completeOptionValues($optionName, CompletionContext $context)
191
	{
192
		$ret = parent::completeOptionValues($optionName, $context);
193
194
		if ( $optionName === 'refs' ) {
195
			return $this->getAllRefs();
196
		}
197
198
		return $ret;
199
	}
200
201
	/**
0 ignored issues
show
introduced by
Doc comment for parameter "$input" missing
Loading history...
introduced by
Doc comment for parameter "$output" missing
Loading history...
202
	 * {@inheritdoc}
203
	 */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
204
	public function initialize(InputInterface $input, OutputInterface $output)
205
	{
206
		parent::initialize($input, $output);
207
208
		$this->_revisionLog = $this->getRevisionLog($this->getWorkingCopyUrl());
209
	}
210
211
	/**
0 ignored issues
show
introduced by
Doc comment for parameter "$input" missing
Loading history...
introduced by
Doc comment for parameter "$output" missing
Loading history...
212
	 * {@inheritdoc}
0 ignored issues
show
introduced by
Doc comment short description must start with a capital letter
Loading history...
213
	 */
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...
214
	protected function execute(InputInterface $input, OutputInterface $output)
215
	{
216
		$bugs = $this->getList($this->io->getOption('bugs'));
217
		$revisions = $this->getList($this->io->getOption('revisions'));
218
219
		if ( $bugs && $revisions ) {
220
			throw new \RuntimeException('The "--bugs" and "--revisions" options are mutually exclusive.');
221
		}
222
223
		$missing_revisions = array();
224
		$revisions_by_path = $this->getRevisionsByPath();
225
226
		if ( $revisions ) {
227
			$revisions = $this->_revisionListParser->expandRanges($revisions);
228
			$revisions_by_path = array_intersect($revisions_by_path, $revisions);
229
			$missing_revisions = array_diff($revisions, $revisions_by_path);
230
		}
231
		elseif ( $bugs ) {
232
			// Only show bug-related revisions on given path. The $missing_revisions is always empty.
233
			$revisions_from_bugs = $this->_revisionLog->find('bugs', $bugs);
234
			$revisions_by_path = array_intersect($revisions_by_path, $revisions_from_bugs);
235
		}
236
237
		$merged_by = $this->getList($this->io->getOption('merged-by'));
238
239
		if ( $merged_by ) {
240
			$merged_by = $this->_revisionListParser->expandRanges($merged_by);
241
			$revisions_by_path = $this->_revisionLog->find('merges', $merged_by);
242
		}
243
244
		if ( $this->io->getOption('merges') ) {
245
			$revisions_by_path = array_intersect($revisions_by_path, $this->_revisionLog->find('merges', 'all_merges'));
246
		}
247
		elseif ( $this->io->getOption('no-merges') ) {
248
			$revisions_by_path = array_diff($revisions_by_path, $this->_revisionLog->find('merges', 'all_merges'));
249
		}
250
251
		if ( $this->io->getOption('merged') ) {
252
			$revisions_by_path = array_intersect($revisions_by_path, $this->_revisionLog->find('merges', 'all_merged'));
253
		}
254
		elseif ( $this->io->getOption('not-merged') ) {
255
			$revisions_by_path = array_diff($revisions_by_path, $this->_revisionLog->find('merges', 'all_merged'));
256
		}
257
258
		if ( $missing_revisions ) {
259
			throw new CommandException($this->getMissingRevisionsErrorMessage($missing_revisions));
260
		}
261
		elseif ( !$revisions_by_path ) {
262
			throw new CommandException('No matching revisions found.');
263
		}
264
265
		rsort($revisions_by_path, SORT_NUMERIC);
266
267
		if ( $bugs || $revisions ) {
268
			// Don't limit revisions, when provided explicitly by user.
269
			$revisions_by_path_with_limit = $revisions_by_path;
270
		}
271
		else {
272
			// Apply limit only, when no explicit bugs/revisions are set.
273
			$revisions_by_path_with_limit = array_slice($revisions_by_path, 0, $this->getMaxCount());
274
		}
275
276
		$revisions_by_path_count = count($revisions_by_path);
277
		$revisions_by_path_with_limit_count = count($revisions_by_path_with_limit);
278
279
		if ( $revisions_by_path_with_limit_count === $revisions_by_path_count ) {
280
			$this->io->writeln(sprintf(
281
				' * Showing <info>%d</info> revision(-s) in %s:',
282
				$revisions_by_path_with_limit_count,
283
				$this->getRevisionLogIdentifier()
284
			));
285
		}
286
		else {
287
			$this->io->writeln(sprintf(
288
				' * Showing <info>%d</info> of <info>%d</info> revision(-s) in %s:',
289
				$revisions_by_path_with_limit_count,
290
				$revisions_by_path_count,
291
				$this->getRevisionLogIdentifier()
292
			));
293
		}
294
295
		$this->printRevisions($revisions_by_path_with_limit);
296
	}
297
298
	/**
299
	 * Returns revision log identifier.
300
	 *
301
	 * @return string
302
	 */
303
	protected function getRevisionLogIdentifier()
304
	{
305
		$ret = '<info>' . $this->_revisionLog->getProjectPath() . '</info> project';
306
307
		$ref_name = $this->_revisionLog->getRefName();
308
309
		if ( $ref_name ) {
310
			$ret .= ' (ref: <info>' . $ref_name . '</info>)';
311
		}
312
		else {
313
			$ret .= ' (all refs)';
314
		}
315
316
		return $ret;
317
	}
318
319
	/**
320
	 * Shows error about missing revisions.
321
	 *
322
	 * @param array $missing_revisions Missing revisions.
323
	 *
324
	 * @return string
325
	 */
326
	protected function getMissingRevisionsErrorMessage(array $missing_revisions)
327
	{
328
		$refs = $this->io->getOption('refs');
329
		$missing_revisions = implode(', ', $missing_revisions);
330
331
		if ( $refs ) {
332
			$revision_source = 'in "' . $refs . '" ref(-s)';
333
		}
334
		else {
335
			$revision_source = 'at "' . $this->getWorkingCopyUrl() . '" url';
336
		}
337
338
		return 'The ' . $missing_revisions . ' revision(-s) not found ' . $revision_source . '.';
339
	}
340
341
	/**
342
	 * Returns list of revisions by path.
343
	 *
344
	 * @return array
345
	 * @throws CommandException When given refs doesn't exist.
346
	 */
347
	protected function getRevisionsByPath()
348
	{
349
		$refs = $this->getList($this->io->getOption('refs'));
350
		$relative_path = $this->repositoryConnector->getRelativePath($this->getWorkingCopyPath()) . '/';
351
352
		if ( !$refs ) {
353
			$ref = $this->repositoryConnector->getRefByPath($relative_path);
354
355
			// Use search by ref, when working copy represents ref root folder.
356
			if ( $ref !== false && preg_match('#' . preg_quote($ref, '#') . '/$#', $relative_path) ) {
357
				return $this->_revisionLog->find('refs', $ref);
358
			}
359
		}
360
361
		if ( $refs ) {
362
			$incorrect_refs = array_diff($refs, $this->getAllRefs());
363
364
			if ( $incorrect_refs ) {
365
				throw new CommandException(
366
					'The following refs are unknown: "' . implode('", "', $incorrect_refs) . '".'
367
				);
368
			}
369
370
			return $this->_revisionLog->find('refs', $refs);
371
		}
372
373
		return $this->_revisionLog->find('paths', $relative_path);
374
	}
375
376
	/**
377
	 * Returns displayed revision limit.
378
	 *
379
	 * @return integer
380
	 */
381
	protected function getMaxCount()
382
	{
383
		$max_count = $this->io->getOption('max-count');
384
385
		if ( $max_count !== null ) {
386
			return $max_count;
387
		}
388
389
		return $this->getSetting(self::SETTING_LOG_LIMIT);
390
	}
391
392
	/**
393
	 * Prints revisions.
394
	 *
395
	 * @param array $revisions Revisions.
396
	 *
397
	 * @return void
398
	 */
399
	protected function printRevisions(array $revisions)
400
	{
401
		$column_mapping = array(
402
			'with-details' => RevisionPrinter::COLUMN_DETAILS,
403
			'with-summary' => RevisionPrinter::COLUMN_SUMMARY,
404
			'with-refs' => RevisionPrinter::COLUMN_REFS,
405
			'with-merge-oracle' => RevisionPrinter::COLUMN_MERGE_ORACLE,
406
			'with-merge-status' => RevisionPrinter::COLUMN_MERGE_STATUS,
407
		);
408
409
		foreach ( $column_mapping as $option_name => $column ) {
410
			if ( $this->io->getOption($option_name) ) {
411
				$this->_revisionPrinter->withColumn($column);
412
			}
413
		}
414
415
		$this->_revisionPrinter->setMergeConflictRegExps($this->getSetting(self::SETTING_LOG_MERGE_CONFLICT_REGEXPS));
416
		$this->_revisionPrinter->setLogMessageLimit($this->getSetting(self::SETTING_LOG_MESSAGE_LIMIT));
417
418
		$this->_revisionPrinter->printRevisions($this->_revisionLog, $revisions, $this->io->getOutput());
419
	}
420
421
	/**
422
	 * Returns list of config settings.
423
	 *
424
	 * @return AbstractConfigSetting[]
425
	 */
426
	public function getConfigSettings()
427
	{
428
		return array(
429
			new IntegerConfigSetting(self::SETTING_LOG_LIMIT, 10),
430
			new IntegerConfigSetting(self::SETTING_LOG_MESSAGE_LIMIT, 68),
431
			new RegExpsConfigSetting(self::SETTING_LOG_MERGE_CONFLICT_REGEXPS, '#/composer\.lock$#'),
432
		);
433
	}
434
435
}
436