Passed
Pull Request — master (#144)
by Alexander
02:08
created

BugsPlugin::doDetectBugs()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

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