Passed
Push — master ( 30ec90...e14136 )
by Alexander
09:22
created

RevisionLog::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 17
ccs 9
cts 9
cp 1
rs 10
cc 1
nc 1
nop 5
crap 1
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\Command;
16
use ConsoleHelpers\SVNBuddy\Repository\Connector\Connector;
17
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\Plugin\DatabaseCollectorPlugin\IDatabaseCollectorPlugin;
18
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\Plugin\IOverwriteAwarePlugin;
19
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\Plugin\IPlugin;
20
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\Plugin\RepositoryCollectorPlugin\IRepositoryCollectorPlugin;
21
use ConsoleHelpers\SVNBuddy\Repository\RevisionUrlBuilder;
22
23
class RevisionLog
24
{
25
26
	const FLAG_VERBOSE = 1;
27
28
	const FLAG_MERGE_HISTORY = 2;
29
30
	/**
31
	 * Repository path.
32
	 *
33
	 * @var string
34
	 */
35
	private $_repositoryRootUrl;
36
37
	/**
38
	 * Project path.
39
	 *
40
	 * @var string
41
	 */
42
	private $_projectPath;
43
44
	/**
45
	 * Ref name.
46
	 *
47
	 * @var string
48
	 */
49
	private $_refName;
50
51
	/**
52
	 * Repository connector.
53
	 *
54
	 * @var Connector
55
	 */
56
	private $_repositoryConnector;
57
58
	/**
59
	 * Console IO.
60
	 *
61
	 * @var ConsoleIO
62
	 */
63
	private $_io;
64
65
	/**
66
	 * Installed plugins.
67
	 *
68
	 * @var IPlugin[]
69
	 */
70
	private $_plugins = array();
71
72
	/**
73
	 * Revision URL builder.
74
	 *
75
	 * @var RevisionUrlBuilder
76
	 */
77
	private $_revisionUrlBuilder;
78
79
	/**
80
	 * Force refresh flag filename.
81
	 *
82
	 * @var string
83
	 */
84
	private $_forceRefreshFlagFilename;
85
86
	/**
87
	 * Create revision log.
88
	 *
89
	 * @param string             $repository_url       Repository url.
90
	 * @param RevisionUrlBuilder $revision_url_builder Revision URL builder.
91
	 * @param Connector          $repository_connector Repository connector.
92
	 * @param string             $working_directory    Working directory.
93
	 * @param ConsoleIO          $io                   Console IO.
94 19
	 */
95
	public function __construct(
96
		$repository_url,
97
		RevisionUrlBuilder $revision_url_builder,
98
		Connector $repository_connector,
99
		$working_directory,
100
		ConsoleIO $io = null
101 19
	) {
102 19
		$this->_io = $io;
103
		$this->_repositoryConnector = $repository_connector;
104 19
105
		$this->_repositoryRootUrl = $repository_connector->getRootUrl($repository_url);
106 19
107 19
		$relative_path = $repository_connector->getRelativePath($repository_url);
108 19
		$this->_projectPath = $repository_connector->getProjectUrl($relative_path) . '/';
109 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...
110 19
		$this->_revisionUrlBuilder = $revision_url_builder;
111 19
		$this->_forceRefreshFlagFilename = $working_directory . '/' . md5($this->_repositoryRootUrl) . '.force-refresh';
112
	}
113
114
	/**
115
	 * Returns revision URL builder.
116
	 *
117
	 * @return RevisionUrlBuilder
118 1
	 */
119
	public function getRevisionURLBuilder()
120 1
	{
121
		return $this->_revisionUrlBuilder;
122
	}
123
124
	/**
125
	 * Queries missing revisions.
126
	 *
127
	 * @param boolean $is_migration Is migration.
128
	 *
129
	 * @return void
130
	 * @throws \LogicException When no plugins are registered.
131 6
	 */
132
	public function refresh($is_migration)
133 6
	{
134 1
		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...
135
			throw new \LogicException('Please register at least one revision log plugin.');
136
		}
137 5
138
		$this->_databaseReady();
139 5
140
		if ( $is_migration ) {
141
			// Import missing data for imported commits only.
142
			$from_revision = 0;
143
			$to_revision = $this->_getAggregateRevision('max');
144
		}
145
		else {
146 5
			// Import all data for new commits only.
147
			$from_revision = $this->_getAggregateRevision('min');
148 5
149
			if ( $this->getForceRefreshFlag() ) {
150
				$this->_repositoryConnector->withCacheOverwrite(true);
151
				$this->setForceRefreshFlag(false);
152
			}
153 5
154
			$to_revision = $this->_repositoryConnector->getLastRevision($this->_repositoryRootUrl);
155
		}
156 5
157 3
		if ( $to_revision > $from_revision ) {
158
			$this->_queryRevisionData($from_revision, $to_revision);
159 5
		}
160
	}
161
162
	/**
163
	 * Sets force refresh flag.
164
	 *
165
	 * @param boolean $flag Flag.
166
	 *
167
	 * @return void
168
	 */
169
	public function setForceRefreshFlag($flag)
170
	{
171
		if ( $flag ) {
172
			touch($this->_forceRefreshFlagFilename);
173
		}
174
		else {
175
			unlink($this->_forceRefreshFlagFilename);
176
		}
177
	}
178
179
	/**
180
	 * Gets force refresh flag.
181
	 *
182
	 * @return boolean
183 5
	 */
184
	protected function getForceRefreshFlag()
185 5
	{
186
		return file_exists($this->_forceRefreshFlagFilename);
187
	}
188
189
	/**
190
	 * Reparses a revision.
191
	 *
192
	 * @param integer $from_revision From revision.
193
	 * @param integer $to_revision   To revision.
194
	 *
195
	 * @return void
196
	 * @throws \LogicException When no plugins are registered.
197
	 */
198
	public function reparse($from_revision, $to_revision)
199
	{
200
		if ( !$this->_plugins ) {
201
			throw new \LogicException('Please register at least one revision log plugin.');
202
		}
203
204
		$this->_databaseReady();
205
		$this->_queryRevisionData($from_revision, $to_revision, true);
206
	}
207
208
	/**
209
	 * Reports to each plugin, that database is ready for usage.
210
	 *
211
	 * @return void
212 5
	 */
213
	private function _databaseReady()
214 5
	{
215 5
		foreach ( $this->_plugins as $plugin ) {
216
			$plugin->whenDatabaseReady();
217 5
		}
218
	}
219
220
	/**
221
	 * Returns aggregated revision from all plugins.
222
	 *
223
	 * @param string $function Aggregate function.
224
	 *
225
	 * @return integer
226 5
	 */
227
	private function _getAggregateRevision($function)
228 5
	{
229
		$last_revisions = array();
230 5
231 5
		foreach ( $this->_plugins as $plugin ) {
232
			$last_revisions[] = $plugin->getLastRevision();
233
		}
234 5
235 5
		if ( count($last_revisions) > 1 ) {
236
			return call_user_func_array($function, $last_revisions);
237
		}
238
239
		return current($last_revisions);
240
	}
241
242
	/**
243
	 * Queries missing revision data.
244
	 *
245
	 * @param integer $from_revision From revision.
246
	 * @param integer $to_revision   To revision.
247
	 * @param boolean $overwrite     Overwrite.
248
	 *
249
	 * @return void
250 3
	 */
251
	private function _queryRevisionData($from_revision, $to_revision, $overwrite = false)
252 3
	{
253 3
		$this->_useRepositoryCollectorPlugins($from_revision, $to_revision, $overwrite);
254
		$this->_useDatabaseCollectorPlugins($from_revision, $to_revision, $overwrite);
255 3
256 1
		if ( isset($this->_io) && $this->_io->isVerbose() ) {
257
			$this->_displayPluginActivityStatistics();
258 3
		}
259
	}
260
261
	/**
262
	 * Use repository collector plugins.
263
	 *
264
	 * @param integer $from_revision From revision.
265
	 * @param integer $to_revision   To revision.
266
	 * @param boolean $overwrite     Overwrite.
267
	 *
268
	 * @return void
269 3
	 */
270
	private function _useRepositoryCollectorPlugins($from_revision, $to_revision, $overwrite = false)
271 3
	{
272
		$batch_size = 500; // Revision count to query in one go.
273
274 3
		// The "io" isn't set during autocomplete.
275
		if ( isset($this->_io) ) {
276 2
			// Create progress bar for repository plugins, where data amount is known upfront.
277 2
			$progress_bar = $this->_io->createProgressBar(ceil(($to_revision - $from_revision) / $batch_size) + 1);
0 ignored issues
show
Bug introduced by
ceil($to_revision - $fro...sion / $batch_size) + 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

277
			$progress_bar = $this->_io->createProgressBar(/** @scrutinizer ignore-type */ ceil(($to_revision - $from_revision) / $batch_size) + 1);
Loading history...
278 2
			$progress_bar->setMessage(
279
				$overwrite ? '* Reparsing revisions:' : ' * Reading missing revisions:'
280 2
			);
281 2
			$progress_bar->setFormat(
282
				'%message% %current%/%max% [%bar%] <info>%percent:3s%%</info> %elapsed:6s%/%estimated:-6s% <info>%memory:-10s%</info>'
283 2
			);
284
			$progress_bar->start();
285
		}
286 3
287
		$plugins = $this->getRepositoryCollectorPlugins($overwrite);
288 3
289
		if ( $overwrite ) {
290
			$this->setPluginsOverwriteMode($plugins, true);
291
		}
292 3
293 3
		$range_start = $from_revision;
294 3
		$cache_duration = $overwrite ? null : '10 years';
295
		$log_command_arguments = $this->_getLogCommandArguments($plugins);
296 3
297 3
		while ( $range_start <= $to_revision ) {
298
			$range_end = min($range_start + ($batch_size - 1), $to_revision);
299 3
300 3
			$command_arguments = str_replace(
301 3
				'{revision_range}',
302 3
				$range_start . ':' . $range_end,
303
				$log_command_arguments
304 3
			);
305 3
			$command = $this->getCommand('log', $command_arguments);
306 3
			$command->setCacheDuration($cache_duration)->setIdleTimeoutRecovery(true);
307
			$svn_log = $command->run();
308 3
309
			$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

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