Failed Conditions
Push — master ( d32373...a3f91c )
by Alexander
01:43
created

Repository/RevisionLog/Plugin/BugsPlugin.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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);
0 ignored issues
show
It seems like $bug_regexp defined by $this->detectProjectBugT...ed'], $cache_overwrite) on line 129 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...
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 ) {
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 4
				$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 3
					$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 ) {
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