console-helpers /
svn-buddy
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
|
|||
| 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 |
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:
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.