Failed Conditions
Pull Request — master (#153)
by Alexander
02:32
created

RevisionLog::_useDatabaseCollectorPlugins()   B

Complexity

Conditions 8
Paths 24

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 8.064

Importance

Changes 0
Metric Value
eloc 19
c 0
b 0
f 0
dl 0
loc 35
ccs 18
cts 20
cp 0.9
rs 8.4444
cc 8
nc 24
nop 3
crap 8.064
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;
12
13
14
use ConsoleHelpers\ConsoleKit\ConsoleIO;
15
use ConsoleHelpers\SVNBuddy\Repository\Connector\Connector;
16
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\Plugin\DatabaseCollectorPlugin\IDatabaseCollectorPlugin;
17
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\Plugin\IOverwriteAwarePlugin;
18
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\Plugin\IPlugin;
19
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\Plugin\RepositoryCollectorPlugin\IRepositoryCollectorPlugin;
20
use ConsoleHelpers\SVNBuddy\Repository\RevisionUrlBuilder;
21
22
class RevisionLog
23
{
24
25
	const FLAG_VERBOSE = 1;
26
27
	const FLAG_MERGE_HISTORY = 2;
28
29
	/**
30
	 * Repository path.
31
	 *
32
	 * @var string
33
	 */
34
	private $_repositoryRootUrl;
35
36
	/**
37
	 * Project path.
38
	 *
39
	 * @var string
40
	 */
41
	private $_projectPath;
42
43
	/**
44
	 * Ref name.
45
	 *
46
	 * @var string
47
	 */
48
	private $_refName;
49
50
	/**
51
	 * Repository connector.
52
	 *
53
	 * @var Connector
54
	 */
55
	private $_repositoryConnector;
56
57
	/**
58
	 * Console IO.
59
	 *
60
	 * @var ConsoleIO
61
	 */
62
	private $_io;
63
64
	/**
65
	 * Installed plugins.
66
	 *
67
	 * @var IPlugin[]
68
	 */
69
	private $_plugins = array();
70
71
	/**
72
	 * Revision URL builder.
73
	 *
74
	 * @var RevisionUrlBuilder
75
	 */
76
	private $_revisionUrlBuilder;
77
78
	/**
79
	 * Create revision log.
80
	 *
81
	 * @param string             $repository_url       Repository url.
82
	 * @param RevisionUrlBuilder $revision_url_builder Revision URL builder.
83
	 * @param Connector          $repository_connector Repository connector.
84
	 * @param ConsoleIO          $io                   Console IO.
85
	 */
86 19
	public function __construct(
87
		$repository_url,
88
		RevisionUrlBuilder $revision_url_builder,
89
		Connector $repository_connector,
90
		ConsoleIO $io = null
91
	) {
92 19
		$this->_io = $io;
93 19
		$this->_repositoryConnector = $repository_connector;
94
95 19
		$this->_repositoryRootUrl = $repository_connector->getRootUrl($repository_url);
96
97 19
		$relative_path = $repository_connector->getRelativePath($repository_url);
98 19
		$this->_projectPath = $repository_connector->getProjectUrl($relative_path) . '/';
99 19
		$this->_refName = $repository_connector->getRefByPath($relative_path);
0 ignored issues
show
Documentation Bug introduced by
It seems like $repository_connector->g...fByPath($relative_path) can also be of type boolean. However, the property $_refName is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
100 19
		$this->_revisionUrlBuilder = $revision_url_builder;
101
	}
102
103
	/**
104
	 * Returns revision URL builder.
105
	 *
106
	 * @return RevisionUrlBuilder
107
	 */
108 1
	public function getRevisionURLBuilder()
109
	{
110 1
		return $this->_revisionUrlBuilder;
111
	}
112
113
	/**
114
	 * Queries missing revisions.
115
	 *
116
	 * @param boolean $is_migration Is migration.
117
	 *
118
	 * @return void
119
	 * @throws \LogicException When no plugins are registered.
120
	 */
121 6
	public function refresh($is_migration)
122
	{
123 6
		if ( !$this->_plugins ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_plugins of type ConsoleHelpers\SVNBuddy\...ionLog\Plugin\IPlugin[] 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...
124 1
			throw new \LogicException('Please register at least one revision log plugin.');
125
		}
126
127 5
		$this->_databaseReady();
128
129 5
		if ( $is_migration ) {
130
			// Import missing data for imported commits only.
131
			$from_revision = 0;
132
			$to_revision = $this->_getAggregateRevision('max');
133
		}
134
		else {
135
			// Import all data for new commits only.
136 5
			$from_revision = $this->_getAggregateRevision('min');
137 5
			$to_revision = $this->_repositoryConnector->getLastRevision($this->_repositoryRootUrl);
138
		}
139
140 5
		if ( $to_revision > $from_revision ) {
141 3
			$this->_queryRevisionData($from_revision, $to_revision);
142
		}
143
	}
144
145
	/**
146
	 * Reparses a revision.
147
	 *
148
	 * @param integer $revision Revision.
149
	 *
150
	 * @return void
151
	 * @throws \LogicException When no plugins are registered.
152
	 */
153
	public function reparse($revision)
154
	{
155
		if ( !$this->_plugins ) {
1 ignored issue
show
Bug Best Practice introduced by
The expression $this->_plugins of type ConsoleHelpers\SVNBuddy\...ionLog\Plugin\IPlugin[] 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...
156
			throw new \LogicException('Please register at least one revision log plugin.');
157
		}
158
159
		$this->_databaseReady();
160
		$this->_queryRevisionData($revision, $revision, true);
161
	}
162
163
	/**
164
	 * Reports to each plugin, that database is ready for usage.
165
	 *
166
	 * @return void
167
	 */
168 5
	private function _databaseReady()
169
	{
170 5
		foreach ( $this->_plugins as $plugin ) {
171 5
			$plugin->whenDatabaseReady();
172
		}
173
	}
174
175
	/**
176
	 * Returns aggregated revision from all plugins.
177
	 *
178
	 * @param string $function Aggregate function.
179
	 *
180
	 * @return integer
181
	 */
182 5
	private function _getAggregateRevision($function)
183
	{
184 5
		$last_revisions = array();
185
186 5
		foreach ( $this->_plugins as $plugin ) {
187 5
			$last_revisions[] = $plugin->getLastRevision();
188
		}
189
190 5
		if ( count($last_revisions) > 1 ) {
191 5
			return call_user_func_array($function, $last_revisions);
192
		}
193
194
		return current($last_revisions);
195
	}
196
197
	/**
198
	 * Queries missing revision data.
199
	 *
200
	 * @param integer $from_revision From revision.
201
	 * @param integer $to_revision   To revision.
202
	 * @param boolean $overwrite     Overwrite.
203
	 *
204
	 * @return void
205
	 */
206 3
	private function _queryRevisionData($from_revision, $to_revision, $overwrite = false)
207
	{
208 3
		$this->_useRepositoryCollectorPlugins($from_revision, $to_revision, $overwrite);
209 3
		$this->_useDatabaseCollectorPlugins($from_revision, $to_revision, $overwrite);
210
211 3
		if ( isset($this->_io) && $this->_io->isVerbose() ) {
212 1
			$this->_displayPluginActivityStatistics();
213
		}
214
	}
215
216
	/**
217
	 * Use repository collector plugins.
218
	 *
219
	 * @param integer $from_revision From revision.
220
	 * @param integer $to_revision   To revision.
221
	 * @param boolean $overwrite     Overwrite.
222
	 *
223
	 * @return void
224
	 */
225 3
	private function _useRepositoryCollectorPlugins($from_revision, $to_revision, $overwrite = false)
226
	{
227
		// The "io" isn't set during autocomplete.
228 3
		if ( isset($this->_io) ) {
229
			// Create progress bar for repository plugins, where data amount is known upfront.
230 2
			$progress_bar = $this->_io->createProgressBar(ceil(($to_revision - $from_revision) / 200) + 1);
0 ignored issues
show
Bug introduced by
ceil($to_revision - $from_revision / 200) + 1 of type double is incompatible with the type integer expected by parameter $max of ConsoleHelpers\ConsoleKi...IO::createProgressBar(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

230
			$progress_bar = $this->_io->createProgressBar(/** @scrutinizer ignore-type */ ceil(($to_revision - $from_revision) / 200) + 1);
Loading history...
231 2
			$progress_bar->setMessage(
232 2
				$overwrite ? '* Reparsing revisions:' : ' * Reading missing revisions:'
233 2
			);
234 2
			$progress_bar->setFormat(
235 2
				'%message% %current%/%max% [%bar%] <info>%percent:3s%%</info> %elapsed:6s%/%estimated:-6s% <info>%memory:-10s%</info>'
236 2
			);
237 2
			$progress_bar->start();
238
		}
239
240 3
		$plugins = $this->getRepositoryCollectorPlugins($overwrite);
241
242 3
		if ( $overwrite ) {
243
			$this->setPluginsOverwriteMode($plugins, true);
244
		}
245
246 3
		$range_start = $from_revision;
247 3
		$cache_duration = $overwrite ? null : '10 years';
248 3
		$log_command_arguments = $this->_getLogCommandArguments($plugins);
249
250 3
		while ( $range_start <= $to_revision ) {
251 3
			$range_end = min($range_start + 199, $to_revision);
252
253 3
			$command_arguments = str_replace(
254 3
				array('{revision_range}', '{repository_url}'),
255 3
				array($range_start . ':' . $range_end, $this->_repositoryRootUrl),
256 3
				$log_command_arguments
257 3
			);
258 3
			$command = $this->_repositoryConnector->getCommand('log', $command_arguments);
259 3
			$command->setCacheDuration($cache_duration);
260 3
			$svn_log = $command->run();
261
262 3
			$this->_parseLog($svn_log, $plugins);
0 ignored issues
show
Bug introduced by
It seems like $svn_log can also be of type string; however, parameter $log of ConsoleHelpers\SVNBuddy\...evisionLog::_parseLog() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

262
			$this->_parseLog(/** @scrutinizer ignore-type */ $svn_log, $plugins);
Loading history...
263
264 3
			$range_start = $range_end + 1;
265
266 3
			if ( isset($progress_bar) ) {
267 2
				$progress_bar->advance();
268
			}
269
		}
270
271
		// Remove progress bar of repository plugins.
272 3
		if ( isset($progress_bar) ) {
273 2
			$progress_bar->clear();
274 2
			unset($progress_bar);
275
		}
276
277 3
		if ( $overwrite ) {
278
			$this->setPluginsOverwriteMode($plugins, false);
279
		}
280
	}
281
282
	/**
283
	 * Use database collector plugins.
284
	 *
285
	 * @param integer $from_revision From revision.
286
	 * @param integer $to_revision   To revision.
287
	 * @param boolean $overwrite     Overwrite.
288
	 *
289
	 * @return void
290
	 */
291 3
	private function _useDatabaseCollectorPlugins($from_revision, $to_revision, $overwrite = false)
292
	{
293 3
		$plugins = $this->getDatabaseCollectorPlugins($overwrite);
294
295 3
		if ( $overwrite ) {
296
			$this->setPluginsOverwriteMode($plugins, true);
297
		}
298
299
		// The "io" isn't set during autocomplete.
300 3
		if ( isset($this->_io) ) {
301
			// Create progress bar for database plugins, where data amount isn't known upfront.
302 2
			$progress_bar = $this->_io->createProgressBar();
303 2
			$progress_bar->setMessage(
304 2
				$overwrite ? '* Reparsing revisions:' : ' * Reading missing revisions:'
305 2
			);
306 2
			$progress_bar->setFormat('%message% %current% [%bar%] %elapsed:6s% <info>%memory:-10s%</info>');
307 2
			$progress_bar->start();
308
309 2
			foreach ( $plugins as $plugin ) {
310 2
				$plugin->process($from_revision, $to_revision, $progress_bar);
311
			}
312
		}
313
		else {
314 1
			foreach ( $plugins as $plugin ) {
315 1
				$plugin->process($from_revision, $to_revision);
316
			}
317
		}
318
319 3
		if ( $overwrite ) {
320
			$this->setPluginsOverwriteMode($plugins, false);
321
		}
322
323 3
		if ( isset($progress_bar) ) {
324 2
			$progress_bar->finish();
325 2
			$this->_io->writeln('');
326
		}
327
	}
328
329
	/**
330
	 * Returns arguments for "log" command.
331
	 *
332
	 * @param IRepositoryCollectorPlugin[] $plugins Plugins.
333
	 *
334
	 * @return array
335
	 */
336 3
	private function _getLogCommandArguments(array $plugins)
337
	{
338 3
		$query_flags = $this->_getRevisionQueryFlags($plugins);
339
340 3
		$ret = array('-r', '{revision_range}', '--xml');
341
342 3
		if ( in_array(self::FLAG_VERBOSE, $query_flags) ) {
343 3
			$ret[] = '--verbose';
344
		}
345
346 3
		if ( in_array(self::FLAG_MERGE_HISTORY, $query_flags) ) {
347 3
			$ret[] = '--use-merge-history';
348
		}
349
350 3
		$ret[] = '{repository_url}';
351
352 3
		return $ret;
353
	}
354
355
	/**
356
	 * Returns revision query flags.
357
	 *
358
	 * @param IRepositoryCollectorPlugin[] $plugins Plugins.
359
	 *
360
	 * @return array
361
	 */
362 3
	private function _getRevisionQueryFlags(array $plugins)
363
	{
364 3
		$ret = array();
365
366 3
		foreach ( $plugins as $plugin ) {
367 3
			$ret = array_merge($ret, $plugin->getRevisionQueryFlags());
368
		}
369
370 3
		return array_unique($ret);
371
	}
372
373
	/**
374
	 * Parses output of "svn log" command.
375
	 *
376
	 * @param \SimpleXMLElement            $log     Log.
377
	 * @param IRepositoryCollectorPlugin[] $plugins Plugins.
378
	 *
379
	 * @return void
380
	 */
381 3
	private function _parseLog(\SimpleXMLElement $log, array $plugins)
382
	{
383 3
		foreach ( $plugins as $plugin ) {
384 3
			$plugin->parse($log);
385
		}
386
	}
387
388
	/**
389
	 * Displays plugin activity statistics.
390
	 *
391
	 * @return void
392
	 */
393 1
	private function _displayPluginActivityStatistics()
394
	{
395 1
		$statistics = array();
396
397
		// Combine statistics from all plugins.
398 1
		foreach ( $this->_plugins as $plugin ) {
399 1
			$statistics = array_merge($statistics, array_filter($plugin->getStatistics()));
400
		}
401
402
		// Show statistics.
403 1
		$this->_io->writeln('<debug>Combined Plugin Statistics:</debug>');
404
405 1
		foreach ( $statistics as $statistic_type => $occurrences ) {
406 1
			$this->_io->writeln('<debug> * ' . $statistic_type . ': ' . $occurrences . '</debug>');
407
		}
408
	}
409
410
	/**
411
	 * Registers a plugin.
412
	 *
413
	 * @param IPlugin $plugin Plugin.
414
	 *
415
	 * @return void
416
	 * @throws \LogicException When plugin is registered several times.
417
	 */
418 12
	public function registerPlugin(IPlugin $plugin)
419
	{
420 12
		$plugin_name = $plugin->getName();
421
422 12
		if ( $this->pluginRegistered($plugin_name) ) {
423 1
			throw new \LogicException('The "' . $plugin_name . '" revision log plugin is already registered.');
424
		}
425
426 12
		$plugin->setRevisionLog($this);
427 12
		$this->_plugins[$plugin_name] = $plugin;
428
	}
429
430
	/**
431
	 * Finds information using plugin.
432
	 *
433
	 * @param string       $plugin_name Plugin name.
434
	 * @param array|string $criteria    Search criteria.
435
	 *
436
	 * @return array
437
	 */
438 3
	public function find($plugin_name, $criteria)
439
	{
440 3
		return $this->getPlugin($plugin_name)->find((array)$criteria, $this->_projectPath);
441
	}
442
443
	/**
444
	 * Returns information about revisions.
445
	 *
446
	 * @param string $plugin_name Plugin name.
447
	 * @param array  $revisions   Revisions.
448
	 *
449
	 * @return array
450
	 */
451 3
	public function getRevisionsData($plugin_name, array $revisions)
452
	{
453 3
		return $this->getPlugin($plugin_name)->getRevisionsData($revisions);
454
	}
455
456
	/**
457
	 * Determines if plugin is registered.
458
	 *
459
	 * @param string $plugin_name Plugin name.
460
	 *
461
	 * @return boolean
462
	 */
463 15
	public function pluginRegistered($plugin_name)
464
	{
465 15
		return array_key_exists($plugin_name, $this->_plugins);
466
	}
467
468
	/**
469
	 * Returns plugin instance.
470
	 *
471
	 * @param string $plugin_name Plugin name.
472
	 *
473
	 * @return IPlugin
474
	 * @throws \InvalidArgumentException When unknown plugin is given.
475
	 */
476 8
	public function getPlugin($plugin_name)
477
	{
478 8
		if ( !$this->pluginRegistered($plugin_name) ) {
479 3
			throw new \InvalidArgumentException('The "' . $plugin_name . '" revision log plugin is unknown.');
480
		}
481
482 5
		return $this->_plugins[$plugin_name];
483
	}
484
485
	/**
486
	 * Returns bugs, from revisions.
487
	 *
488
	 * @param array $revisions Revisions.
489
	 *
490
	 * @return array
491
	 */
492 1
	public function getBugsFromRevisions(array $revisions)
493
	{
494 1
		$bugs = array();
495 1
		$revisions_bugs = $this->getRevisionsData('bugs', $revisions);
496
497 1
		foreach ( $revisions as $revision ) {
498 1
			$revision_bugs = $revisions_bugs[$revision];
499
500 1
			foreach ( $revision_bugs as $bug_id ) {
501 1
				$bugs[$bug_id] = true;
502
			}
503
		}
504
505 1
		return array_keys($bugs);
506
	}
507
508
	/**
509
	 * Returns repository collector plugins.
510
	 *
511
	 * @param boolean $overwrite_mode Overwrite mode.
512
	 *
513
	 * @return IRepositoryCollectorPlugin[]
514
	 */
515 3
	protected function getRepositoryCollectorPlugins($overwrite_mode)
516
	{
517 3
		$plugins = $this->getPluginsByInterface(IRepositoryCollectorPlugin::class);
518
519 3
		if ( !$overwrite_mode ) {
520 3
			return $plugins;
521
		}
522
523
		return $this->getPluginsByInterface(IOverwriteAwarePlugin::class, $plugins);
524
	}
525
526
	/**
527
	 * Returns database collector plugins.
528
	 *
529
	 * @param boolean $overwrite_mode Overwrite mode.
530
	 *
531
	 * @return IDatabaseCollectorPlugin[]
532
	 */
533 3
	protected function getDatabaseCollectorPlugins($overwrite_mode)
534
	{
535 3
		$plugins = $this->getPluginsByInterface(IDatabaseCollectorPlugin::class);
536
537 3
		if ( !$overwrite_mode ) {
538 3
			return $plugins;
539
		}
540
541
		return $this->getPluginsByInterface(IOverwriteAwarePlugin::class, $plugins);
542
	}
543
544
	/**
545
	 * Returns plugin list filtered by interface.
546
	 *
547
	 * @param string    $interface Interface name.
548
	 * @param IPlugin[] $plugins   Plugins.
549
	 *
550
	 * @return IPlugin[]
551
	 */
552 3
	protected function getPluginsByInterface($interface, array $plugins = array())
553
	{
554 3
		if ( !$plugins ) {
555 3
			$plugins = $this->_plugins;
556
		}
557
558 3
		$ret = array();
559
560 3
		foreach ( $plugins as $plugin ) {
561 3
			if ( $plugin instanceof $interface ) {
562 3
				$ret[] = $plugin;
563
			}
564
		}
565
566 3
		return $ret;
567
	}
568
569
	/**
570
	 * Sets overwrite mode.
571
	 *
572
	 * @param IOverwriteAwarePlugin[] $plugins        Plugins.
573
	 * @param boolean                 $overwrite_mode Overwrite mode.
574
	 *
575
	 * @return void
576
	 */
577
	protected function setPluginsOverwriteMode(array $plugins, $overwrite_mode)
578
	{
579
		foreach ( $plugins as $plugin ) {
580
			$plugin->setOverwriteMode($overwrite_mode);
581
		}
582
	}
583
584
	/**
585
	 * Returns project path.
586
	 *
587
	 * @return string
588
	 */
589 1
	public function getProjectPath()
590
	{
591 1
		return $this->_projectPath;
592
	}
593
594
	/**
595
	 * Returns ref name.
596
	 *
597
	 * @return string
598
	 */
599 1
	public function getRefName()
600
	{
601 1
		return $this->_refName;
602
	}
603
604
}
605