Passed
Pull Request — master (#144)
by Alexander
02:08
created

RevisionLog::_parseLog()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
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\IDatabaseCollectorPlugin;
17
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\Plugin\IPlugin;
18
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\Plugin\IRepositoryCollectorPlugin;
19
use ConsoleHelpers\SVNBuddy\Repository\RevisionUrlBuilder;
20
21
class RevisionLog
22
{
23
24
	const FLAG_VERBOSE = 1;
25
26
	const FLAG_MERGE_HISTORY = 2;
27
28
	/**
29
	 * Repository path.
30
	 *
31
	 * @var string
32
	 */
33
	private $_repositoryRootUrl;
34
35
	/**
36
	 * Project path.
37
	 *
38
	 * @var string
39
	 */
40
	private $_projectPath;
41
42
	/**
43
	 * Ref name.
44
	 *
45
	 * @var string
46
	 */
47
	private $_refName;
48
49
	/**
50
	 * Repository connector.
51
	 *
52
	 * @var Connector
53
	 */
54
	private $_repositoryConnector;
55
56
	/**
57
	 * Console IO.
58
	 *
59
	 * @var ConsoleIO
60
	 */
61
	private $_io;
62
63
	/**
64
	 * Installed plugins.
65
	 *
66
	 * @var IPlugin[]
67
	 */
68
	private $_plugins = array();
69
70
	/**
71
	 * Revision URL builder.
72
	 *
73
	 * @var RevisionUrlBuilder
74
	 */
75
	private $_revisionUrlBuilder;
76
77
	/**
78
	 * Create revision log.
79
	 *
80
	 * @param string             $repository_url       Repository url.
81
	 * @param RevisionUrlBuilder $revision_url_builder Revision URL builder.
82
	 * @param Connector          $repository_connector Repository connector.
83
	 * @param ConsoleIO          $io                   Console IO.
84
	 */
85 19
	public function __construct(
86
		$repository_url,
87
		RevisionUrlBuilder $revision_url_builder,
88
		Connector $repository_connector,
89
		ConsoleIO $io = null
90
	) {
91 19
		$this->_io = $io;
92 19
		$this->_repositoryConnector = $repository_connector;
93
94 19
		$this->_repositoryRootUrl = $repository_connector->getRootUrl($repository_url);
95
96 19
		$relative_path = $repository_connector->getRelativePath($repository_url);
97 19
		$this->_projectPath = $repository_connector->getProjectUrl($relative_path) . '/';
98 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...
99 19
		$this->_revisionUrlBuilder = $revision_url_builder;
100 19
	}
101
102
	/**
103
	 * Returns revision URL builder.
104
	 *
105
	 * @return RevisionUrlBuilder
106
	 */
107 1
	public function getRevisionURLBuilder()
108
	{
109 1
		return $this->_revisionUrlBuilder;
110
	}
111
112
	/**
113
	 * Queries missing revisions.
114
	 *
115
	 * @param boolean $is_migration Is migration.
116
	 *
117
	 * @return void
118
	 * @throws \LogicException When no plugins are registered.
119
	 */
120 6
	public function refresh($is_migration)
121
	{
122 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...
123 1
			throw new \LogicException('Please register at least one revision log plugin.');
124
		}
125
126 5
		$this->_databaseReady();
127
128 5
		if ( $is_migration ) {
129
			// Import missing data for imported commits only.
130
			$from_revision = 0;
131
			$to_revision = $this->_getAggregateRevision('max');
132
		}
133
		else {
134
			// Import all data for new commits only.
135 5
			$from_revision = $this->_getAggregateRevision('min');
136 5
			$to_revision = $this->_repositoryConnector->getLastRevision($this->_repositoryRootUrl);
137
		}
138
139 5
		if ( $to_revision > $from_revision ) {
140 3
			$this->_queryRevisionData($from_revision, $to_revision);
141
		}
142 5
	}
143
144
	/**
145
	 * Reports to each plugin, that database is ready for usage.
146
	 *
147
	 * @return void
148
	 */
149 5
	private function _databaseReady()
150
	{
151 5
		foreach ( $this->_plugins as $plugin ) {
152 5
			$plugin->whenDatabaseReady();
153
		}
154 5
	}
155
156
	/**
157
	 * Returns aggregated revision from all plugins.
158
	 *
159
	 * @param string $function Aggregate function.
160
	 *
161
	 * @return integer
162
	 */
163 5
	private function _getAggregateRevision($function)
164
	{
165 5
		$last_revisions = array();
166
167 5
		foreach ( $this->_plugins as $plugin ) {
168 5
			$last_revisions[] = $plugin->getLastRevision();
169
		}
170
171 5
		if ( count($last_revisions) > 1 ) {
172 5
			return call_user_func_array($function, $last_revisions);
173
		}
174
175
		return current($last_revisions);
176
	}
177
178
	/**
179
	 * Queries missing revision data.
180
	 *
181
	 * @param integer $from_revision From revision.
182
	 * @param integer $to_revision   To revision.
183
	 *
184
	 * @return void
185
	 */
186 3
	private function _queryRevisionData($from_revision, $to_revision)
187
	{
188 3
		$range_start = $from_revision;
189
190
		// The "io" isn't set during autocomplete.
191 3
		if ( isset($this->_io) ) {
192
			// Create progress bar for repository plugins, where data amount is known upfront.
193 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

193
			$progress_bar = $this->_io->createProgressBar(/** @scrutinizer ignore-type */ ceil(($to_revision - $from_revision) / 200) + 1);
Loading history...
194 2
			$progress_bar->setMessage(' * Reading missing revisions:');
195 2
			$progress_bar->setFormat(
196 2
				'%message% %current%/%max% [%bar%] <info>%percent:3s%%</info> %elapsed:6s%/%estimated:-6s% <info>%memory:-10s%</info>'
197
			);
198 2
			$progress_bar->start();
199
		}
200
201 3
		$log_command_arguments = $this->_getLogCommandArguments();
202 3
		$is_verbose = isset($this->_io) && $this->_io->isVerbose();
203
204 3
		while ( $range_start <= $to_revision ) {
205 3
			$range_end = min($range_start + 199, $to_revision);
206
207 3
			$command_arguments = str_replace(
208 3
				array('{revision_range}', '{repository_url}'),
209 3
				array($range_start . ':' . $range_end, $this->_repositoryRootUrl),
210 3
				$log_command_arguments
211
			);
212 3
			$command = $this->_repositoryConnector->getCommand('log', $command_arguments);
213 3
			$command->setCacheDuration('10 years');
214 3
			$svn_log = $command->run();
215
216 3
			$this->_parseLog($svn_log);
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

216
			$this->_parseLog(/** @scrutinizer ignore-type */ $svn_log);
Loading history...
217
218 3
			$range_start = $range_end + 1;
219
220 3
			if ( isset($progress_bar) ) {
221 2
				$progress_bar->advance();
222
			}
223
		}
224
225 3
		if ( isset($progress_bar) ) {
226
			// Remove progress bar of repository plugins.
227 2
			$progress_bar->clear();
228 2
			unset($progress_bar);
229
230
			// Create progress bar for database plugins, where data amount isn't known upfront.
231 2
			$progress_bar = $this->_io->createProgressBar();
232 2
			$progress_bar->setMessage(' * Reading missing revisions:');
233 2
			$progress_bar->setFormat('%message% %current% [%bar%] %elapsed:6s% <info>%memory:-10s%</info>');
234 2
			$progress_bar->start();
235
236 2
			foreach ( $this->getDatabaseCollectorPlugins() as $plugin ) {
237 2
				$plugin->process($from_revision, $to_revision, $progress_bar);
238
			}
239
		}
240
		else {
241 1
			foreach ( $this->getDatabaseCollectorPlugins() as $plugin ) {
242 1
				$plugin->process($from_revision, $to_revision);
243
			}
244
		}
245
246 3
		if ( isset($progress_bar) ) {
247 2
			$progress_bar->finish();
248 2
			$this->_io->writeln('');
249
		}
250
251 3
		if ( $is_verbose ) {
252 1
			$this->_displayPluginActivityStatistics();
253
		}
254 3
	}
255
256
	/**
257
	 * Returns arguments for "log" command.
258
	 *
259
	 * @return array
260
	 */
261 3
	private function _getLogCommandArguments()
262
	{
263 3
		$query_flags = $this->_getRevisionQueryFlags();
264
265 3
		$ret = array('-r', '{revision_range}', '--xml');
266
267 3
		if ( in_array(self::FLAG_VERBOSE, $query_flags) ) {
268 3
			$ret[] = '--verbose';
269
		}
270
271 3
		if ( in_array(self::FLAG_MERGE_HISTORY, $query_flags) ) {
272 3
			$ret[] = '--use-merge-history';
273
		}
274
275 3
		$ret[] = '{repository_url}';
276
277 3
		return $ret;
278
	}
279
280
	/**
281
	 * Returns revision query flags.
282
	 *
283
	 * @return array
284
	 */
285 3
	private function _getRevisionQueryFlags()
286
	{
287 3
		$ret = array();
288
289 3
		foreach ( $this->getRepositoryCollectorPlugins() as $plugin ) {
290 3
			$ret = array_merge($ret, $plugin->getRevisionQueryFlags());
291
		}
292
293 3
		return array_unique($ret);
294
	}
295
296
	/**
297
	 * Parses output of "svn log" command.
298
	 *
299
	 * @param \SimpleXMLElement $log Log.
300
	 *
301
	 * @return void
302
	 */
303 3
	private function _parseLog(\SimpleXMLElement $log)
304
	{
305 3
		foreach ( $this->getRepositoryCollectorPlugins() as $plugin ) {
306 3
			$plugin->parse($log);
307
		}
308 3
	}
309
310
	/**
311
	 * Displays plugin activity statistics.
312
	 *
313
	 * @return void
314
	 */
315 1
	private function _displayPluginActivityStatistics()
316
	{
317 1
		$statistics = array();
318
319
		// Combine statistics from all plugins.
320 1
		foreach ( $this->_plugins as $plugin ) {
321 1
			$statistics = array_merge($statistics, array_filter($plugin->getStatistics()));
322
		}
323
324
		// Show statistics.
325 1
		$this->_io->writeln('<debug>Combined Plugin Statistics:</debug>');
326
327 1
		foreach ( $statistics as $statistic_type => $occurrences ) {
328 1
			$this->_io->writeln('<debug> * ' . $statistic_type . ': ' . $occurrences . '</debug>');
329
		}
330 1
	}
331
332
	/**
333
	 * Registers a plugin.
334
	 *
335
	 * @param IPlugin $plugin Plugin.
336
	 *
337
	 * @return void
338
	 * @throws \LogicException When plugin is registered several times.
339
	 */
340 12
	public function registerPlugin(IPlugin $plugin)
341
	{
342 12
		$plugin_name = $plugin->getName();
343
344 12
		if ( $this->pluginRegistered($plugin_name) ) {
345 1
			throw new \LogicException('The "' . $plugin_name . '" revision log plugin is already registered.');
346
		}
347
348 12
		$plugin->setRevisionLog($this);
349 12
		$this->_plugins[$plugin_name] = $plugin;
350 12
	}
351
352
	/**
353
	 * Finds information using plugin.
354
	 *
355
	 * @param string       $plugin_name Plugin name.
356
	 * @param array|string $criteria    Search criteria.
357
	 *
358
	 * @return array
359
	 */
360 3
	public function find($plugin_name, $criteria)
361
	{
362 3
		return $this->getPlugin($plugin_name)->find((array)$criteria, $this->_projectPath);
363
	}
364
365
	/**
366
	 * Returns information about revisions.
367
	 *
368
	 * @param string $plugin_name Plugin name.
369
	 * @param array  $revisions   Revisions.
370
	 *
371
	 * @return array
372
	 */
373 3
	public function getRevisionsData($plugin_name, array $revisions)
374
	{
375 3
		return $this->getPlugin($plugin_name)->getRevisionsData($revisions);
376
	}
377
378
	/**
379
	 * Determines if plugin is registered.
380
	 *
381
	 * @param string $plugin_name Plugin name.
382
	 *
383
	 * @return boolean
384
	 */
385 15
	public function pluginRegistered($plugin_name)
386
	{
387 15
		return array_key_exists($plugin_name, $this->_plugins);
388
	}
389
390
	/**
391
	 * Returns plugin instance.
392
	 *
393
	 * @param string $plugin_name Plugin name.
394
	 *
395
	 * @return IPlugin
396
	 * @throws \InvalidArgumentException When unknown plugin is given.
397
	 */
398 8
	public function getPlugin($plugin_name)
399
	{
400 8
		if ( !$this->pluginRegistered($plugin_name) ) {
401 3
			throw new \InvalidArgumentException('The "' . $plugin_name . '" revision log plugin is unknown.');
402
		}
403
404 5
		return $this->_plugins[$plugin_name];
405
	}
406
407
	/**
408
	 * Returns bugs, from revisions.
409
	 *
410
	 * @param array $revisions Revisions.
411
	 *
412
	 * @return array
413
	 */
414 1
	public function getBugsFromRevisions(array $revisions)
415
	{
416 1
		$bugs = array();
417 1
		$revisions_bugs = $this->getRevisionsData('bugs', $revisions);
418
419 1
		foreach ( $revisions as $revision ) {
420 1
			$revision_bugs = $revisions_bugs[$revision];
421
422 1
			foreach ( $revision_bugs as $bug_id ) {
423 1
				$bugs[$bug_id] = true;
424
			}
425
		}
426
427 1
		return array_keys($bugs);
428
	}
429
430
	/**
431
	 * Returns repository collector plugins.
432
	 *
433
	 * @return IRepositoryCollectorPlugin[]
434
	 */
435 3
	protected function getRepositoryCollectorPlugins()
436
	{
437 3
		return $this->getPluginsByInterface(
438 3
			'ConsoleHelpers\SVNBuddy\Repository\RevisionLog\Plugin\IRepositoryCollectorPlugin'
439
		);
440
	}
441
442
	/**
443
	 * Returns database collector plugins.
444
	 *
445
	 * @return IDatabaseCollectorPlugin[]
446
	 */
447 3
	protected function getDatabaseCollectorPlugins()
448
	{
449 3
		return $this->getPluginsByInterface(
450 3
			'ConsoleHelpers\SVNBuddy\Repository\RevisionLog\Plugin\IDatabaseCollectorPlugin'
451
		);
452
	}
453
454
	/**
455
	 * Returns plugin list filtered by interface.
456
	 *
457
	 * @param string $interface Interface name.
458
	 *
459
	 * @return IPlugin[]
460
	 */
461 3
	protected function getPluginsByInterface($interface)
462
	{
463 3
		$ret = array();
464
465 3
		foreach ( $this->_plugins as $plugin ) {
466 3
			if ( $plugin instanceof $interface ) {
467 3
				$ret[] = $plugin;
468
			}
469
		}
470
471 3
		return $ret;
472
	}
473
474
	/**
475
	 * Returns project path.
476
	 *
477
	 * @return string
478
	 */
479 1
	public function getProjectPath()
480
	{
481 1
		return $this->_projectPath;
482
	}
483
484
	/**
485
	 * Returns ref name.
486
	 *
487
	 * @return string
488
	 */
489 1
	public function getRefName()
490
	{
491 1
		return $this->_refName;
492
	}
493
494
}
495