Completed
Push — master ( 06984a...96a0c3 )
by Alexander
32s queued 27s
created

BugsPlugin::getLastChangedRefPaths()   B

Complexity

Conditions 6
Paths 14

Size

Total Lines 48
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 6

Importance

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