SearchCommand::getSearchEfficiency()   B
last analyzed

Complexity

Conditions 11
Paths 6

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 11
c 1
b 0
f 0
dl 0
loc 23
ccs 0
cts 12
cp 0
rs 7.3166
cc 11
nc 6
nop 1
crap 132

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\ConsoleKit\Exception\CommandException;
15
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\RevisionLog;
16
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
17
use Symfony\Component\Console\Input\InputArgument;
18
use Symfony\Component\Console\Input\InputInterface;
19
use Symfony\Component\Console\Input\InputOption;
20
use Symfony\Component\Console\Output\OutputInterface;
21
22
class SearchCommand extends AbstractCommand
23
{
24
25
	const MATCH_TYPE_FIRST = 'first';
26
27
	const MATCH_TYPE_LAST = 'last';
28
29
	/**
30
	 * Revision log
31
	 *
32
	 * @var RevisionLog
33
	 */
34
	private $_revisionLog;
35
36
	/**
37
	 * {@inheritdoc}
38
	 */
39
	protected function configure()
40
	{
41
		$this
42
			->setName('search')
43
			->setDescription('Searches for a revision, where text was added to a file or removed from it')
44
			->addArgument(
45
				'path',
46
				InputArgument::REQUIRED,
47
				'File path'
48
			)
49
			->addArgument(
50
				'keywords',
51
				InputArgument::REQUIRED,
52
				'Search keyword'
53
			)
54
			->addOption(
55
				'match-type',
56
				't',
57
				InputOption::VALUE_REQUIRED,
58
				'Match type, e.g. <comment>first</comment> or <comment>last</comment>',
59
				self::MATCH_TYPE_LAST
60
			);
61
62
		parent::configure();
63
	}
64
65
	/**
66
	 * Return possible values for the named option
67
	 *
68
	 * @param string            $optionName Option name.
69
	 * @param CompletionContext $context    Completion context.
70
	 *
71
	 * @return array
72
	 */
73
	public function completeOptionValues($optionName, CompletionContext $context)
74
	{
75
		$ret = parent::completeOptionValues($optionName, $context);
76
77
		if ( $optionName === 'match-type' ) {
78
			return $this->getMatchTypes();
79
		}
80
81
		return $ret;
82
	}
83
84
	/**
85
	 * {@inheritdoc}
86
	 */
87
	public function initialize(InputInterface $input, OutputInterface $output)
88
	{
89
		parent::initialize($input, $output);
90
91
		$this->_revisionLog = $this->getRevisionLog($this->getWorkingCopyUrl());
92
	}
93
94
	/**
95
	 * {@inheritdoc}
96
	 *
97
	 * @throws \RuntimeException When invalid direction is specified.
98
	 * @throws \RuntimeException When keywords are empty.
99
	 * @throws CommandException When no revisions found for path specified.
100
	 */
101
	protected function execute(InputInterface $input, OutputInterface $output)
102
	{
103
		$match_type = $this->io->getOption('match-type');
104
105
		if ( !in_array($match_type, $this->getMatchTypes()) ) {
106
			throw new \RuntimeException('The "' . $match_type . '" match type is invalid.');
107
		}
108
109
		$wc_path = $this->getWorkingCopyPath();
110
		$relative_path = $this->repositoryConnector->getRelativePath($wc_path);
111
112
		$keywords = trim($this->io->getArgument('keywords'));
0 ignored issues
show
Bug introduced by
It seems like $this->io->getArgument('keywords') can also be of type null and string[]; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

112
		$keywords = trim(/** @scrutinizer ignore-type */ $this->io->getArgument('keywords'));
Loading history...
113
114
		if ( !strlen($keywords) ) {
115
			throw new \RuntimeException('The "keywords" argument is empty or contains only whitespaces.');
116
		}
117
118
		$this->io->writeln(sprintf(
119
			'Searching for %s match of "<info>%s</info>" in "<info>%s</info>":',
120
			$match_type,
0 ignored issues
show
Bug introduced by
It seems like $match_type can also be of type string[]; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

120
			/** @scrutinizer ignore-type */ $match_type,
Loading history...
121
			$keywords,
122
			'.../' . basename($relative_path)
123
		));
124
125
		$revisions = $this->_revisionLog->find('paths', $relative_path);
126
127
		if ( !$revisions ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $revisions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
128
			throw new CommandException('No revisions found for "' . $relative_path . '" path.');
129
		}
130
131
		if ( $match_type === self::MATCH_TYPE_LAST ) {
132
			$revisions = array_reverse($revisions);
133
		}
134
135
		$scanned_revisions = 0;
136
		$total_revisions = count($revisions);
137
138
		$progress_bar = $this->io->createProgressBar($total_revisions);
139
		$progress_bar->setFormat(
140
			'<info>%message:6s%</info> %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%'
141
		);
142
143
		$index = 0;
144
		$found_revision = false;
145
146
		foreach ( $revisions as $index => $revision ) {
147
			$scanned_revisions++;
148
			$progress_bar->setMessage('r' . $revision);
149
			$progress_bar->display();
150
151
			$file_content = $this->repositoryConnector->getFileContent($wc_path, $revision);
152
153
			if ( strpos($file_content, $keywords) !== false ) {
154
				$found_revision = $revision;
155
				break;
156
			}
157
158
			$progress_bar->advance();
159
		}
160
161
		$this->io->writeln('');
162
163
		$scanned_percent = round(($scanned_revisions / $total_revisions) * 100);
164
		$this->io->writeln('Search efficiency: ' . $this->getSearchEfficiency(100 - $scanned_percent));
165
166
		if ( $found_revision ) {
167
			if ( $match_type === self::MATCH_TYPE_LAST ) {
168
				$this->io->writeln('Last seen at ' . $this->getRevisionInfo($found_revision) . '.');
169
170
				if ( array_key_exists($index - 1, $revisions) ) {
171
					$this->io->writeln('Deleted at ' . $this->getRevisionInfo($revisions[$index - 1]) . '.');
172
				}
173
			}
174
			else {
175
				$this->io->writeln('First seen at ' . $this->getRevisionInfo($found_revision) . '.');
176
			}
177
178
			return;
179
		}
180
181
		$this->io->writeln('<error>Keywords not found.</error>');
182
	}
183
184
	/**
185
	 * Returns match types.
186
	 *
187
	 * @return array
188
	 */
189
	protected function getMatchTypes()
190
	{
191
		return array(self::MATCH_TYPE_FIRST, self::MATCH_TYPE_LAST);
192
	}
193
194
	/**
195
	 * Returns search efficiency.
196
	 *
197
	 * @param float $percent Percent.
198
	 *
199
	 * @return string
200
	 */
201
	protected function getSearchEfficiency($percent)
202
	{
203
		if ( $percent >= 0 && $percent <= 20 ) {
204
			return '<fg=red>Very Poor</>';
205
		}
206
207
		if ( $percent > 20 && $percent <= 40 ) {
208
			return '<fg=red;options=bold>Poor</>';
209
		}
210
211
		if ( $percent > 40 && $percent <= 60 ) {
212
			return '<fg=yellow>Fair</>';
213
		}
214
215
		if ( $percent > 60 && $percent <= 80 ) {
216
			return '<fg=green;options=bold>Good</>';
217
		}
218
219
		if ( $percent > 80 && $percent <= 100 ) {
220
			return '<fg=green>Excellent</>';
221
		}
222
223
		return '';
224
	}
225
226
	/**
227
	 * Get revision information.
228
	 *
229
	 * @param integer $revision Revision.
230
	 *
231
	 * @return string
232
	 */
233
	protected function getRevisionInfo($revision)
234
	{
235
		$bugs = $this->_revisionLog->getRevisionsData('bugs', array($revision));
236
237
		$ret = '<info>' . $revision . '</info>';
238
239
		if ( $bugs[$revision] ) {
240
			$ret .= ' (bugs: <info>' . implode('</info>, <info>', $bugs[$revision]) . '</info>)';
241
		}
242
243
		return $ret;
244
	}
245
246
}
247