Failed Conditions
Push — master ( 598f4b...15edf4 )
by Alexander
04:31
created

BugsPlugin::getProjectBugRegExps()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 16
ccs 9
cts 9
cp 1
rs 9.4285
cc 3
eloc 8
nc 3
nop 0
crap 3
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 19
	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 19
		parent::__construct($database, $repository_filler);
63
64 19
		$this->_repositoryUrl = $repository_url;
65 19
		$this->_repositoryConnector = $repository_connector;
66 19
		$this->_logMessageParserFactory = $log_message_parser_factory;
67 19
	}
68
69
	/**
70
	 * Returns plugin name.
71
	 *
72
	 * @return string
73
	 */
74 13
	public function getName()
75
	{
76 13
		return 'bugs';
77
	}
78
79
	/**
80
	 * Defines parsing statistic types.
81
	 *
82
	 * @return array
83
	 */
84 19
	public function defineStatisticTypes()
85
	{
86
		return array(
87 19
			self::STATISTIC_BUG_ADDED_TO_COMMIT,
88 19
		);
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 9
	public function doProcess($from_revision, $to_revision)
100
	{
101 9
		$this->populateMissingBugRegExp();
102
103 9
		$last_revision = $this->getLastRevision();
104
105 9
		if ( $to_revision > $last_revision ) {
106 7
			$this->detectBugs($last_revision + 1, $to_revision);
107 7
			$this->setLastRevision($to_revision);
108 7
		}
109 9
	}
110
111
	/**
112
	 * Populate "BugRegExp" column for projects without it.
113
	 *
114
	 * @return void
115
	 */
116 9
	protected function populateMissingBugRegExp()
117
	{
118 9
		$projects = $this->getProjects('BugRegExp IS NULL');
119
120 9
		if ( !$projects ) {
121 5
			$this->advanceProgressBar();
122
123 5
			return;
124
		}
125
126 4
		foreach ( $projects as $project_data ) {
127 4
			$bug_regexp = $this->detectProjectBugTraqRegEx(
128 4
				$project_data['Path'],
129 4
				$project_data['RevisionLastSeen'],
130 4
				(bool)$project_data['IsDeleted']
131 4
			);
132
133 4
			$this->repositoryFiller->setProjectBugRegexp($project_data['Id'], $bug_regexp);
0 ignored issues
show
Bug introduced by
It seems like $bug_regexp defined by $this->detectProjectBugT...ject_data['IsDeleted']) on line 127 can also be of type object<SimpleXMLElement>; however, ConsoleHelpers\SVNBuddy\...::setProjectBugRegexp() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
134 4
			$this->advanceProgressBar();
135 4
		}
136 4
	}
137
138
	/**
139
	 * Determines project bug tracking regular expression.
140
	 *
141
	 * @param string  $project_path    Project project_path.
142
	 * @param integer $revision        Revision.
143
	 * @param boolean $project_deleted Project is deleted.
144
	 *
145
	 * @return string
146
	 */
147 4
	protected function detectProjectBugTraqRegEx($project_path, $revision, $project_deleted)
148
	{
149 4
		$ref_path = $this->getLastChangedRefPath($project_path);
150
151 4
		if ( !isset($ref_path) ) {
152 2
			return '';
153
		}
154
155 2
		return $this->_repositoryConnector
156 2
			->withCache('1 year')
157 2
			->getProperty(
158 2
				'bugtraq:logregex',
159 2
				$this->_repositoryUrl . $ref_path . ($project_deleted ? '@' . $revision : '')
160 2
			);
161
	}
162
163
	/**
164
	 * Returns last changed ref within given project.
165
	 *
166
	 * @param string $project_path Path.
167
	 *
168
	 * @return string|null
169
	 */
170 4
	protected function getLastChangedRefPath($project_path)
171
	{
172 4
		$own_nesting_level = substr_count($project_path, '/') - 1;
173
174
		$sql = 'SELECT Path, RevisionLastSeen
175
				FROM Paths
176 4
				WHERE Path LIKE :parent_path AND PathNestingLevel BETWEEN :from_level AND :to_level';
177 4
		$paths = $this->database->fetchPairs($sql, array(
178 4
			'parent_path' => $project_path . '%',
179 4
			'from_level' => $own_nesting_level + 1,
180 4
			'to_level' => $own_nesting_level + 2,
181 4
		));
182
183
		// No sub-folders.
184 4
		if ( !$paths ) {
185 1
			return null;
186
		}
187
188 3
		$filtered_paths = array();
189
190 3
		foreach ( $paths as $path => $revision ) {
191 3
			if ( $this->isRef($path) ) {
192 2
				$filtered_paths[$path] = $revision;
193 2
			}
194 3
		}
195
196
		// None of sub-folders matches a ref.
197 3
		if ( !$filtered_paths ) {
198 1
			return null;
199
		}
200
201 2
		arsort($filtered_paths, SORT_NUMERIC);
202
203 2
		return key($filtered_paths);
204
	}
205
206
	/**
207
	 * Detects if given project_path is known project root.
208
	 *
209
	 * @param string $path Path.
210
	 *
211
	 * @return boolean
212
	 */
213 3
	protected function isRef($path)
214
	{
215
		// Not a folder.
216 3
		if ( substr($path, -1, 1) !== '/' ) {
217 2
			return false;
218
		}
219
220 3
		$ref = $this->_repositoryConnector->getRefByPath($path);
221
222
		// Not a ref.
223 3
		if ( $ref === false ) {
224 2
			return false;
225
		}
226
227 3
		return preg_match('/\/' . preg_quote($ref, '/') . '\/$/', $path) > 0;
228
	}
229
230
	/**
231
	 * Detects bugs, associated with each commit from a given revision range.
232
	 *
233
	 * @param integer $from_revision From revision.
234
	 * @param integer $to_revision   To revision.
235
	 *
236
	 * @return void
237
	 */
238 7
	protected function detectBugs($from_revision, $to_revision)
239
	{
240 7
		$bug_regexp_mapping = $this->getProjectBugRegExps();
241
242 7
		if ( !$bug_regexp_mapping ) {
243 3
			$this->advanceProgressBar();
244
245 3
			return;
246
		}
247
248 4
		$range_start = $from_revision;
249
250 4
		while ( $range_start <= $to_revision ) {
251 4
			$range_end = min($range_start + 999, $to_revision);
252
253 4
			$this->doDetectBugs($range_start, $range_end, $bug_regexp_mapping);
254 4
			$this->advanceProgressBar();
255
256 4
			$range_start = $range_end + 1;
257 4
		}
258 4
	}
259
260
	/**
261
	 * Returns "BugRegExp" field associated with every project.
262
	 *
263
	 * @return array
264
	 */
265 7
	protected function getProjectBugRegExps()
266
	{
267 7
		$projects = $this->getProjects("BugRegExp != ''");
268
269 7
		if ( !$projects ) {
270 3
			return array();
271
		}
272
273 4
		$ret = array();
274
275 4
		foreach ( $projects as $project_data ) {
276 4
			$ret[$project_data['Id']] = $project_data['BugRegExp'];
277 4
		}
278
279 4
		return $ret;
280
	}
281
282
	/**
283
	 * Detects bugs, associated with each commit from a given revision range.
284
	 *
285
	 * @param integer $from_revision      From revision.
286
	 * @param integer $to_revision        To revision.
287
	 * @param array   $bug_regexp_mapping Mapping between project and it's "BugRegExp" field.
288
	 *
289
	 * @return void
290
	 */
291 4
	protected function doDetectBugs($from_revision, $to_revision, array $bug_regexp_mapping)
292
	{
293 4
		$commits_by_project = $this->getCommitsGroupedByProject($from_revision, $to_revision);
294
295 4
		foreach ( $commits_by_project as $project_id => $project_commits ) {
296 4
			if ( !isset($bug_regexp_mapping[$project_id]) ) {
297 1
				continue;
298
			}
299
300 4
			$log_message_parser = $this->_logMessageParserFactory->getLogMessageParser(
301 4
				$bug_regexp_mapping[$project_id]
302 4
			);
303
304 4
			foreach ( $project_commits as $revision => $log_message ) {
305 4
				$bugs = $log_message_parser->parse($log_message);
306
307 4
				if ( $bugs ) {
308 3
					$this->repositoryFiller->addBugsToCommit($bugs, $revision);
309 3
					$this->recordStatistic(self::STATISTIC_BUG_ADDED_TO_COMMIT, count($bugs));
310 3
				}
311 4
			}
312 4
		}
313 4
	}
314
315
	/**
316
	 * Returns commits grouped by project.
317
	 *
318
	 * @param integer $from_revision From revision.
319
	 * @param integer $to_revision   To revision.
320
	 *
321
	 * @return array
322
	 */
323 4
	protected function getCommitsGroupedByProject($from_revision, $to_revision)
324
	{
325
		$sql = 'SELECT cp.Revision, c.Message, cp.ProjectId
326
				FROM CommitProjects cp
327
				JOIN Commits c ON c.Revision = cp.Revision
328 4
				WHERE cp.Revision BETWEEN :from_revision AND :to_revision';
329 4
		$commits = new AllIterator($this->database->perform($sql, array(
330 4
			'from_revision' => $from_revision,
331 4
			'to_revision' => $to_revision,
332 4
		)));
333
334 4
		$ret = array();
335 4
		$processed_revisions = array();
336
337 4
		foreach ( $commits as $commit_data ) {
338 4
			$revision = $commit_data['Revision'];
339
340
			// Don't process revision more then once (e.g. when commit belongs to different projects).
341 4
			if ( isset($processed_revisions[$revision]) ) {
342 1
				continue;
343
			}
344
345 4
			$project_id = $commit_data['ProjectId'];
346
347 4
			if ( !isset($ret[$project_id]) ) {
348 4
				$ret[$project_id] = array();
349 4
			}
350
351 4
			$ret[$project_id][$revision] = $commit_data['Message'];
352 4
			$processed_revisions[$revision] = true;
353 4
		}
354
355 4
		return $ret;
356
	}
357
358
	/**
359
	 * Find revisions by collected data.
360
	 *
361
	 * @param array  $criteria     Criteria.
362
	 * @param string $project_path Project path.
363
	 *
364
	 * @return array
365
	 */
366 5
	public function find(array $criteria, $project_path)
367
	{
368 5
		if ( !$criteria ) {
369 1
			return array();
370
		}
371
372 4
		$project_id = $this->getProject($project_path);
373
374
		$sql = 'SELECT DISTINCT cb.Revision
375
				FROM CommitBugs cb
376
				JOIN CommitProjects cp ON cp.Revision = cb.Revision
377 3
				WHERE cp.ProjectId = :project_id AND cb.Bug IN (:bugs)';
378 3
		$bug_revisions = $this->database->fetchCol($sql, array('project_id' => $project_id, 'bugs' => $criteria));
379
380 3
		sort($bug_revisions, SORT_NUMERIC);
381
382 3
		return $bug_revisions;
383
	}
384
385
	/**
386
	 * Returns information about revisions.
387
	 *
388
	 * @param array $revisions Revisions.
389
	 *
390
	 * @return array
391
	 */
392 1
	public function getRevisionsData(array $revisions)
393
	{
394 1
		$results = array();
395
396
		$sql = 'SELECT Revision, Bug
397
				FROM CommitBugs
398 1
				WHERE Revision IN (:revisions)';
399 1
		$revisions_data = $this->database->fetchAll($sql, array('revisions' => $revisions));
400
401 1
		foreach ( $revisions_data as $revision_data ) {
402 1
			$revision = $revision_data['Revision'];
403 1
			$bug = $revision_data['Bug'];
404
405 1
			if ( !isset($results[$revision]) ) {
406 1
				$results[$revision] = array();
407 1
			}
408
409 1
			$results[$revision][] = $bug;
410 1
		}
411
412 1
		return $this->addMissingResults($revisions, $results);
413
	}
414
415
}
416