Passed
Push — master ( a1e767...742af1 )
by Alexander
17:40 queued 15:37
created

BugsPlugin::detectProjectBugTraqRegEx()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5.0113

Importance

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