Failed Conditions
Pull Request — master (#153)
by Alexander
02:32
created

BugsPlugin   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 452
Duplicated Lines 0 %

Test Coverage

Coverage 99.38%

Importance

Changes 0
Metric Value
wmc 45
eloc 140
c 0
b 0
f 0
dl 0
loc 452
ccs 160
cts 161
cp 0.9938
rs 8.8

16 Methods

Rating   Name   Duplication   Size   Complexity  
A doProcess() 0 16 4
A isRef() 0 8 2
A populateMissingBugRegExp() 0 20 3
A getCommitsGroupedByProject() 0 33 4
A defineStatisticTypes() 0 4 1
A doDetectBugs() 0 19 5
A getName() 0 3 1
A getLastChangedRefPaths() 0 40 5
A getProjectBugRegExps() 0 15 3
A getRevisionsData() 0 21 3
A detectProjectBugTraqRegEx() 0 22 5
A remove() 0 5 2
A __construct() 0 12 1
A detectBugs() 0 19 3
A find() 0 17 2
A refreshBugRegExp() 0 12 1

How to fix   Complexity   

Complex Class

Complex classes like BugsPlugin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BugsPlugin, and based on these observations, apply Extract Interface, too.

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\Repository\RevisionLog\Plugin\DatabaseCollectorPlugin;
12
13
14
use Aura\Sql\ExtendedPdoInterface;
15
use ConsoleHelpers\SVNBuddy\Repository\Connector\Connector;
16
use ConsoleHelpers\SVNBuddy\Repository\Parser\LogMessageParserFactory;
17
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\Plugin\IOverwriteAwarePlugin;
18
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\Plugin\TOverwriteAwarePlugin;
19
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\RepositoryFiller;
20
21
class BugsPlugin extends AbstractDatabaseCollectorPlugin implements IOverwriteAwarePlugin
22
{
23
24
	use TOverwriteAwarePlugin;
25
26
	const STATISTIC_BUG_ADDED_TO_COMMIT = 'bug_added_to_commit';
27
28
	const STATISTIC_BUG_REMOVED_FROM_COMMIT = 'bug_removed_from_commit';
29
30
	/**
31
	 * Repository url.
32
	 *
33
	 * @var string
34
	 */
35
	private $_repositoryUrl;
36
37
	/**
38
	 * Repository connector.
39
	 *
40
	 * @var Connector
41
	 */
42
	private $_repositoryConnector;
43
44
	/**
45
	 * Log message parser factory.
46
	 *
47
	 * @var LogMessageParserFactory
48
	 */
49
	private $_logMessageParserFactory;
50
51
	/**
52
	 * Create bugs revision log plugin.
53
	 *
54
	 * @param ExtendedPdoInterface    $database                   Database.
55
	 * @param RepositoryFiller        $repository_filler          Repository filler.
56
	 * @param string                  $repository_url             Repository url.
57
	 * @param Connector               $repository_connector       Repository connector.
58
	 * @param LogMessageParserFactory $log_message_parser_factory Log message parser.
59
	 */
60 24
	public function __construct(
61
		ExtendedPdoInterface $database,
62
		RepositoryFiller $repository_filler,
63
		$repository_url,
64
		Connector $repository_connector,
65
		LogMessageParserFactory $log_message_parser_factory
66
	) {
67 24
		parent::__construct($database, $repository_filler);
68
69 24
		$this->_repositoryUrl = $repository_url;
70 24
		$this->_repositoryConnector = $repository_connector;
71 24
		$this->_logMessageParserFactory = $log_message_parser_factory;
72
	}
73
74
	/**
75
	 * Returns plugin name.
76
	 *
77
	 * @return string
78
	 */
79 17
	public function getName()
80
	{
81 17
		return 'bugs';
82
	}
83
84
	/**
85
	 * Defines parsing statistic types.
86
	 *
87
	 * @return array
88
	 */
89 24
	public function defineStatisticTypes()
90
	{
91 24
		return array(
92 24
			self::STATISTIC_BUG_ADDED_TO_COMMIT, self::STATISTIC_BUG_REMOVED_FROM_COMMIT,
93 24
		);
94
	}
95
96
	/**
97
	 * Processes data.
98
	 *
99
	 * @param integer $from_revision From revision.
100
	 * @param integer $to_revision   To revision.
101
	 *
102
	 * @return void
103
	 */
104 13
	public function doProcess($from_revision, $to_revision)
105
	{
106 13
		$this->populateMissingBugRegExp();
107
108 13
		$last_revision = $this->getLastRevision();
109
110 13
		if ( $this->isOverwriteMode() ) {
111 1
			$this->remove($from_revision, $to_revision);
112 1
			$this->detectBugs($from_revision, $to_revision);
113
		}
114 12
		elseif ( $to_revision > $last_revision ) {
115 7
			$this->detectBugs($last_revision + 1, $to_revision);
116
		}
117
118 13
		if ( $to_revision > $last_revision ) {
119 7
			$this->setLastRevision($to_revision);
120
		}
121
	}
122
123
	/**
124
	 * Removes changes plugin made based on a given revision.
125
	 *
126
	 * @param integer $from_revision From revision.
127
	 * @param integer $to_revision   To revision.
128
	 *
129
	 * @return void
130
	 */
131 1
	protected function remove($from_revision, $to_revision)
132
	{
133 1
		for ( $revision = $from_revision; $revision <= $to_revision; $revision++ ) {
134 1
			$bug_count = $this->repositoryFiller->removeBugsFromCommit($revision);
135 1
			$this->recordStatistic(self::STATISTIC_BUG_REMOVED_FROM_COMMIT, $bug_count);
136
		}
137
	}
138
139
	/**
140
	 * Populate "BugRegExp" column for projects without it.
141
	 *
142
	 * @param boolean $cache_overwrite Overwrite used "bugtraq:logregex" SVN property's cached value.
143
	 *
144
	 * @return void
145
	 */
146 13
	protected function populateMissingBugRegExp($cache_overwrite = false)
147
	{
148 13
		$projects = $this->getProjects('BugRegExp IS NULL');
149
150 13
		if ( !$projects ) {
151 7
			$this->advanceProgressBar();
152
153 7
			return;
154
		}
155
156 6
		foreach ( $projects as $project_data ) {
157 6
			$bug_regexp = $this->detectProjectBugTraqRegEx(
158 6
				$project_data['Path'],
159 6
				$project_data['RevisionLastSeen'],
160 6
				(bool)$project_data['IsDeleted'],
161 6
				$cache_overwrite
162 6
			);
163
164 6
			$this->repositoryFiller->setProjectBugRegexp($project_data['Id'], $bug_regexp);
165 6
			$this->advanceProgressBar();
166
		}
167
	}
168
169
	/**
170
	 * Determines project bug tracking regular expression.
171
	 *
172
	 * @param string  $project_path    Project project_path.
173
	 * @param integer $revision        Revision.
174
	 * @param boolean $project_deleted Project is deleted.
175
	 * @param boolean $cache_overwrite Overwrite used "bugtraq:logregex" SVN property's cached value.
176
	 *
177
	 * @return string
178
	 */
179 6
	protected function detectProjectBugTraqRegEx($project_path, $revision, $project_deleted, $cache_overwrite = false)
180
	{
181 6
		$ref_paths = $this->getLastChangedRefPaths($project_path);
182
183 6
		if ( !$ref_paths ) {
184 2
			return '';
185
		}
186
187 4
		foreach ( $ref_paths as $ref_path ) {
188 4
			$logregex = $this->_repositoryConnector
189 4
				->withCache('1 year', $cache_overwrite)
190 4
				->getProperty(
191 4
					'bugtraq:logregex',
192 4
					$this->_repositoryUrl . $ref_path . ($project_deleted ? '@' . $revision : '')
193 4
				);
194
195 4
			if ( strlen($logregex) ) {
196 4
				return $logregex;
197
			}
198
		}
199
200
		return '';
201
	}
202
203
	/**
204
	 * Returns given project refs, where last changed are on top.
205
	 *
206
	 * @param string $project_path Path.
207
	 *
208
	 * @return array
209
	 */
210 6
	protected function getLastChangedRefPaths($project_path)
211
	{
212 6
		$own_nesting_level = substr_count($project_path, '/') - 1;
213
214 6
		$where_clause = array(
215 6
			'Path LIKE :parent_path',
216 6
			'PathNestingLevel BETWEEN :from_level AND :to_level',
217 6
			'RevisionDeleted IS NULL',
218 6
		);
219
220 6
		$sql = 'SELECT Path, RevisionLastSeen
221
				FROM Paths
222 6
				WHERE (' . implode(') AND (', $where_clause) . ')';
223 6
		$paths = $this->database->fetchPairs($sql, array(
224 6
			'parent_path' => $project_path . '%',
225 6
			'from_level' => $own_nesting_level + 1,
226 6
			'to_level' => $own_nesting_level + 2,
227 6
		));
228
229
		// No sub-folders.
230 6
		if ( !$paths ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $paths 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...
231 1
			return array();
232
		}
233
234 5
		$filtered_paths = array();
235
236 5
		foreach ( $paths as $path => $revision ) {
237 5
			if ( $this->isRef($path) ) {
238 4
				$filtered_paths[$path] = $revision;
239
			}
240
		}
241
242
		// None of sub-folders matches a ref.
243 5
		if ( !$filtered_paths ) {
244 1
			return array();
245
		}
246
247 4
		arsort($filtered_paths, SORT_NUMERIC);
248
249 4
		return array_keys($filtered_paths);
250
	}
251
252
	/**
253
	 * Detects if given project_path is known project root.
254
	 *
255
	 * @param string $path Path.
256
	 *
257
	 * @return boolean
258
	 */
259 5
	protected function isRef($path)
260
	{
261
		// Not a folder.
262 5
		if ( substr($path, -1, 1) !== '/' ) {
263 4
			return false;
264
		}
265
266 5
		return $this->_repositoryConnector->isRefRoot($path);
267
	}
268
269
	/**
270
	 * Detects bugs, associated with each commit from a given revision range.
271
	 *
272
	 * @param integer $from_revision From revision.
273
	 * @param integer $to_revision   To revision.
274
	 *
275
	 * @return void
276
	 */
277 8
	protected function detectBugs($from_revision, $to_revision)
278
	{
279 8
		$bug_regexp_mapping = $this->getProjectBugRegExps();
280
281 8
		if ( !$bug_regexp_mapping ) {
282 3
			$this->advanceProgressBar();
283
284 3
			return;
285
		}
286
287 5
		$range_start = $from_revision;
288
289 5
		while ( $range_start <= $to_revision ) {
290 5
			$range_end = min($range_start + 999, $to_revision);
291
292 5
			$this->doDetectBugs($range_start, $range_end, $bug_regexp_mapping);
293 5
			$this->advanceProgressBar();
294
295 5
			$range_start = $range_end + 1;
296
		}
297
	}
298
299
	/**
300
	 * Returns "BugRegExp" field associated with every project.
301
	 *
302
	 * @return array
303
	 */
304 8
	protected function getProjectBugRegExps()
305
	{
306 8
		$projects = $this->getProjects("BugRegExp != ''");
307
308 8
		if ( !$projects ) {
309 3
			return array();
310
		}
311
312 5
		$ret = array();
313
314 5
		foreach ( $projects as $project_data ) {
315 5
			$ret[$project_data['Id']] = $project_data['BugRegExp'];
316
		}
317
318 5
		return $ret;
319
	}
320
321
	/**
322
	 * Detects bugs, associated with each commit from a given revision range.
323
	 *
324
	 * @param integer $from_revision      From revision.
325
	 * @param integer $to_revision        To revision.
326
	 * @param array   $bug_regexp_mapping Mapping between project and it's "BugRegExp" field.
327
	 *
328
	 * @return void
329
	 */
330 5
	protected function doDetectBugs($from_revision, $to_revision, array $bug_regexp_mapping)
331
	{
332 5
		$commits_by_project = $this->getCommitsGroupedByProject($from_revision, $to_revision);
333
334 5
		foreach ( $commits_by_project as $project_id => $project_commits ) {
335 5
			if ( !isset($bug_regexp_mapping[$project_id]) ) {
336 1
				continue;
337
			}
338
339 5
			$log_message_parser = $this->_logMessageParserFactory->getLogMessageParser(
340 5
				$bug_regexp_mapping[$project_id]
341 5
			);
342
343 5
			foreach ( $project_commits as $revision => $log_message ) {
344 5
				$bugs = $log_message_parser->parse($log_message);
345
346 5
				if ( $bugs ) {
347 4
					$this->repositoryFiller->addBugsToCommit($bugs, $revision);
348 4
					$this->recordStatistic(self::STATISTIC_BUG_ADDED_TO_COMMIT, count($bugs));
349
				}
350
			}
351
		}
352
	}
353
354
	/**
355
	 * Returns commits grouped by project.
356
	 *
357
	 * @param integer $from_revision From revision.
358
	 * @param integer $to_revision   To revision.
359
	 *
360
	 * @return array
361
	 */
362 5
	protected function getCommitsGroupedByProject($from_revision, $to_revision)
363
	{
364 5
		$sql = 'SELECT cp.Revision, c.Message, cp.ProjectId
365
				FROM CommitProjects cp
366
				JOIN Commits c ON c.Revision = cp.Revision
367 5
				WHERE cp.Revision BETWEEN :from_revision AND :to_revision';
368 5
		$commits = $this->database->yieldAll($sql, array(
369 5
			'from_revision' => $from_revision,
370 5
			'to_revision' => $to_revision,
371 5
		));
372
373 5
		$ret = array();
374 5
		$processed_revisions = array();
375
376 5
		foreach ( $commits as $commit_data ) {
377 5
			$revision = $commit_data['Revision'];
378
379
			// Don't process revision more then once (e.g. when commit belongs to different projects).
380 5
			if ( isset($processed_revisions[$revision]) ) {
381 1
				continue;
382
			}
383
384 5
			$project_id = $commit_data['ProjectId'];
385
386 5
			if ( !isset($ret[$project_id]) ) {
387 5
				$ret[$project_id] = array();
388
			}
389
390 5
			$ret[$project_id][$revision] = $commit_data['Message'];
391 5
			$processed_revisions[$revision] = true;
392
		}
393
394 5
		return $ret;
395
	}
396
397
	/**
398
	 * Find revisions by collected data.
399
	 *
400
	 * @param array  $criteria     Criteria.
401
	 * @param string $project_path Project path.
402
	 *
403
	 * @return array
404
	 */
405 5
	public function find(array $criteria, $project_path)
406
	{
407 5
		if ( !$criteria ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $criteria 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...
408 1
			return array();
409
		}
410
411 4
		$project_id = $this->getProject($project_path);
412
413 3
		$sql = 'SELECT DISTINCT cb.Revision
414
				FROM CommitBugs cb
415
				JOIN CommitProjects cp ON cp.Revision = cb.Revision
416 3
				WHERE cp.ProjectId = :project_id AND cb.Bug IN (:bugs)';
417 3
		$bug_revisions = $this->database->fetchCol($sql, array('project_id' => $project_id, 'bugs' => $criteria));
418
419 3
		sort($bug_revisions, SORT_NUMERIC);
420
421 3
		return $bug_revisions;
422
	}
423
424
	/**
425
	 * Returns information about revisions.
426
	 *
427
	 * @param array $revisions Revisions.
428
	 *
429
	 * @return array
430
	 */
431 1
	public function getRevisionsData(array $revisions)
432
	{
433 1
		$results = array();
434
435 1
		$sql = 'SELECT Revision, Bug
436
				FROM CommitBugs
437 1
				WHERE Revision IN (:revisions)';
438 1
		$revisions_data = $this->database->fetchAll($sql, array('revisions' => $revisions));
439
440 1
		foreach ( $revisions_data as $revision_data ) {
441 1
			$revision = $revision_data['Revision'];
442 1
			$bug = $revision_data['Bug'];
443
444 1
			if ( !isset($results[$revision]) ) {
445 1
				$results[$revision] = array();
446
			}
447
448 1
			$results[$revision][] = $bug;
449
		}
450
451 1
		return $this->addMissingResults($revisions, $results);
452
	}
453
454
	/**
455
	 * Refreshes BugRegExp of a project.
456
	 *
457
	 * @param string $project_path Project path.
458
	 *
459
	 * @return void
460
	 */
461 2
	public function refreshBugRegExp($project_path)
462
	{
463 2
		$project_id = $this->getProject($project_path);
464
465 2
		$sql = 'UPDATE Projects
466
				SET BugRegExp = NULL
467 2
				WHERE Id = :project_id';
468 2
		$this->database->perform($sql, array(
469 2
			'project_id' => $project_id,
470 2
		));
471
472 2
		$this->populateMissingBugRegExp(true);
473
	}
474
475
}
476