Completed
Push — master ( 12cab7...06984a )
by Alexander
31s queued 27s
created

BugsPlugin::detectProjectBugTraqRegEx()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.009

Importance

Changes 0
Metric Value
eloc 13
c 0
b 0
f 0
dl 0
loc 23
ccs 13
cts 14
cp 0.9286
rs 9.5222
cc 5
nc 4
nop 4
crap 5.009
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 24
	}
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
		return array(
92 24
			self::STATISTIC_BUG_ADDED_TO_COMMIT, self::STATISTIC_BUG_REMOVED_FROM_COMMIT,
93
		);
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 13
	}
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 1
	}
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
			);
163
164 6
			$this->repositoryFiller->setProjectBugRegexp($project_data['Id'], $bug_regexp);
165 6
			$this->advanceProgressBar();
166
		}
167 6
	}
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
				->withCacheDuration('1 year')
190 4
				->withCacheOverwrite($cache_overwrite)
191 4
				->getProperty(
192 4
					'bugtraq:logregex',
193 4
					$this->_repositoryUrl . $ref_path . ($project_deleted ? '@' . $revision : '')
194
				);
195
196 4
			if ( strlen($logregex) ) {
197 4
				return $logregex;
198
			}
199
		}
200
201
		return '';
202
	}
203
204
	/**
205
	 * Returns given project refs, where last changed are on top.
206
	 *
207
	 * @param string $project_path Path.
208
	 *
209
	 * @return array
210
	 */
211 6
	protected function getLastChangedRefPaths($project_path)
212
	{
213 6
		$own_nesting_level = substr_count($project_path, '/') - 1;
214
215
		$where_clause = array(
216 6
			'Path LIKE :parent_path',
217
			'PathNestingLevel BETWEEN :from_level AND :to_level',
218
			'RevisionDeleted IS NULL',
219
		);
220
221
		$sql = 'SELECT Path, RevisionLastSeen
222
				FROM Paths
223 6
				WHERE (' . implode(') AND (', $where_clause) . ')';
224 6
		$paths = $this->database->fetchPairs($sql, array(
225 6
			'parent_path' => $project_path . '%',
226 6
			'from_level' => $own_nesting_level + 1,
227 6
			'to_level' => $own_nesting_level + 2,
228
		));
229
230
		// No sub-folders.
231 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...
232 1
			return array();
233
		}
234
235 5
		$filtered_paths = array();
236
237 5
		foreach ( $paths as $path => $revision ) {
238 5
			if ( $this->isRef($path) ) {
239 5
				$filtered_paths[$path] = $revision;
240
			}
241
		}
242
243
		// None of sub-folders matches a ref.
244 5
		if ( !$filtered_paths ) {
245 1
			return array();
246
		}
247
248 4
		arsort($filtered_paths, SORT_NUMERIC);
249
250 4
		return array_keys($filtered_paths);
251
	}
252
253
	/**
254
	 * Detects if given project_path is known project root.
255
	 *
256
	 * @param string $path Path.
257
	 *
258
	 * @return boolean
259
	 */
260 5
	protected function isRef($path)
261
	{
262
		// Not a folder.
263 5
		if ( substr($path, -1, 1) !== '/' ) {
264 4
			return false;
265
		}
266
267 5
		return $this->_repositoryConnector->isRefRoot($path);
268
	}
269
270
	/**
271
	 * Detects bugs, associated with each commit from a given revision range.
272
	 *
273
	 * @param integer $from_revision From revision.
274
	 * @param integer $to_revision   To revision.
275
	 *
276
	 * @return void
277
	 */
278 8
	protected function detectBugs($from_revision, $to_revision)
279
	{
280 8
		$bug_regexp_mapping = $this->getProjectBugRegExps();
281
282 8
		if ( !$bug_regexp_mapping ) {
283 3
			$this->advanceProgressBar();
284
285 3
			return;
286
		}
287
288 5
		$range_start = $from_revision;
289
290 5
		while ( $range_start <= $to_revision ) {
291 5
			$range_end = min($range_start + 999, $to_revision);
292
293 5
			$this->doDetectBugs($range_start, $range_end, $bug_regexp_mapping);
294 5
			$this->advanceProgressBar();
295
296 5
			$range_start = $range_end + 1;
297
		}
298 5
	}
299
300
	/**
301
	 * Returns "BugRegExp" field associated with every project.
302
	 *
303
	 * @return array
304
	 */
305 8
	protected function getProjectBugRegExps()
306
	{
307 8
		$projects = $this->getProjects("BugRegExp != ''");
308
309 8
		if ( !$projects ) {
310 3
			return array();
311
		}
312
313 5
		$ret = array();
314
315 5
		foreach ( $projects as $project_data ) {
316 5
			$ret[$project_data['Id']] = $project_data['BugRegExp'];
317
		}
318
319 5
		return $ret;
320
	}
321
322
	/**
323
	 * Detects bugs, associated with each commit from a given revision range.
324
	 *
325
	 * @param integer $from_revision      From revision.
326
	 * @param integer $to_revision        To revision.
327
	 * @param array   $bug_regexp_mapping Mapping between project and it's "BugRegExp" field.
328
	 *
329
	 * @return void
330
	 */
331 5
	protected function doDetectBugs($from_revision, $to_revision, array $bug_regexp_mapping)
332
	{
333 5
		$commits_by_project = $this->getCommitsGroupedByProject($from_revision, $to_revision);
334
335 5
		foreach ( $commits_by_project as $project_id => $project_commits ) {
336 5
			if ( !isset($bug_regexp_mapping[$project_id]) ) {
337 1
				continue;
338
			}
339
340 5
			$log_message_parser = $this->_logMessageParserFactory->getLogMessageParser(
341 5
				$bug_regexp_mapping[$project_id]
342
			);
343
344 5
			foreach ( $project_commits as $revision => $log_message ) {
345 5
				$bugs = $log_message_parser->parse($log_message);
346
347 5
				if ( $bugs ) {
348 4
					$this->repositoryFiller->addBugsToCommit($bugs, $revision);
349 5
					$this->recordStatistic(self::STATISTIC_BUG_ADDED_TO_COMMIT, count($bugs));
350
				}
351
			}
352
		}
353 5
	}
354
355
	/**
356
	 * Returns commits grouped by project.
357
	 *
358
	 * @param integer $from_revision From revision.
359
	 * @param integer $to_revision   To revision.
360
	 *
361
	 * @return array
362
	 */
363 5
	protected function getCommitsGroupedByProject($from_revision, $to_revision)
364
	{
365 5
		$sql = 'SELECT cp.Revision, c.Message, cp.ProjectId
366
				FROM CommitProjects cp
367
				JOIN Commits c ON c.Revision = cp.Revision
368
				WHERE cp.Revision BETWEEN :from_revision AND :to_revision';
369 5
		$commits = $this->database->yieldAll($sql, array(
370 5
			'from_revision' => $from_revision,
371 5
			'to_revision' => $to_revision,
372
		));
373
374 5
		$ret = array();
375 5
		$processed_revisions = array();
376
377 5
		foreach ( $commits as $commit_data ) {
378 5
			$revision = $commit_data['Revision'];
379
380
			// Don't process revision more then once (e.g. when commit belongs to different projects).
381 5
			if ( isset($processed_revisions[$revision]) ) {
382 1
				continue;
383
			}
384
385 5
			$project_id = $commit_data['ProjectId'];
386
387 5
			if ( !isset($ret[$project_id]) ) {
388 5
				$ret[$project_id] = array();
389
			}
390
391 5
			$ret[$project_id][$revision] = $commit_data['Message'];
392 5
			$processed_revisions[$revision] = true;
393
		}
394
395 5
		return $ret;
396
	}
397
398
	/**
399
	 * Find revisions by collected data.
400
	 *
401
	 * @param array  $criteria     Criteria.
402
	 * @param string $project_path Project path.
403
	 *
404
	 * @return array
405
	 */
406 5
	public function find(array $criteria, $project_path)
407
	{
408 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...
409 1
			return array();
410
		}
411
412 4
		$project_id = $this->getProject($project_path);
413
414 3
		$sql = 'SELECT DISTINCT cb.Revision
415
				FROM CommitBugs cb
416
				JOIN CommitProjects cp ON cp.Revision = cb.Revision
417
				WHERE cp.ProjectId = :project_id AND cb.Bug IN (:bugs)';
418 3
		$bug_revisions = $this->database->fetchCol($sql, array('project_id' => $project_id, 'bugs' => $criteria));
419
420 3
		sort($bug_revisions, SORT_NUMERIC);
421
422 3
		return $bug_revisions;
423
	}
424
425
	/**
426
	 * Returns information about revisions.
427
	 *
428
	 * @param array $revisions Revisions.
429
	 *
430
	 * @return array
431
	 */
432 1
	public function getRevisionsData(array $revisions)
433
	{
434 1
		$results = array();
435
436 1
		$sql = 'SELECT Revision, Bug
437
				FROM CommitBugs
438
				WHERE Revision IN (:revisions)';
439 1
		$revisions_data = $this->getRawRevisionsData($sql, 'revisions', $revisions);
440
441 1
		foreach ( $revisions_data as $revision_data ) {
442 1
			$revision = $revision_data['Revision'];
443 1
			$bug = $revision_data['Bug'];
444
445 1
			if ( !isset($results[$revision]) ) {
446 1
				$results[$revision] = array();
447
			}
448
449 1
			$results[$revision][] = $bug;
450
		}
451
452 1
		return $this->addMissingResults($revisions, $results);
453
	}
454
455
	/**
456
	 * Refreshes BugRegExp of a project.
457
	 *
458
	 * @param string $project_path Project path.
459
	 *
460
	 * @return void
461
	 */
462 2
	public function refreshBugRegExp($project_path)
463
	{
464 2
		$project_id = $this->getProject($project_path);
465
466 2
		$sql = 'UPDATE Projects
467
				SET BugRegExp = NULL
468
				WHERE Id = :project_id';
469 2
		$this->database->perform($sql, array(
470 2
			'project_id' => $project_id,
471
		));
472
473 2
		$this->populateMissingBugRegExp(true);
474 2
	}
475
476
}
477