PathsPlugin   F
last analyzed

Complexity

Total Complexity 88

Size/Duplication

Total Lines 945
Duplicated Lines 0 %

Test Coverage

Coverage 99.45%

Importance

Changes 0
Metric Value
wmc 88
eloc 325
c 0
b 0
f 0
dl 0
loc 945
ccs 361
cts 363
cp 0.9945
rs 2

29 Methods

Rating   Name   Duplication   Size   Complexity  
B doParse() 0 55 10
A whenDatabaseReady() 0 7 1
A defineStatisticTypes() 0 13 1
A getName() 0 3 1
A initDatabaseCache() 0 5 1
A __construct() 0 14 1
A getRevisionQueryFlags() 0 3 1
A findByKind() 0 12 1
A processRef() 0 36 4
A findByAction() 0 12 1
A adaptPathToKind() 0 7 2
A processFieldCriterion() 0 20 5
A findAllRevisions() 0 8 1
A getRefId() 0 9 1
A findByExactMatch() 0 46 5
A remove() 0 3 1
A addMissingCommitsToProject() 0 23 3
A sortPaths() 0 11 3
A addCommitToProject() 0 4 1
A getPathCopyData() 0 24 2
B find() 0 39 7
A freeMemoryManually() 0 5 1
A getPathFromId() 0 8 1
A getPathId() 0 8 1
B processProject() 0 39 6
A getRevisionsData() 0 30 3
C processPath() 0 79 14
B findBySubMatch() 0 74 8
A addCommitToRef() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like PathsPlugin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PathsPlugin, and based on these observations, apply Extract Interface, too.

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\RepositoryCollectorPlugin;
12
13
14
use Aura\Sql\ExtendedPdoInterface;
15
use ConsoleHelpers\SVNBuddy\Database\DatabaseCache;
16
use ConsoleHelpers\SVNBuddy\Repository\Connector\Connector;
17
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\PathCollisionDetector;
18
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\RepositoryFiller;
19
use ConsoleHelpers\SVNBuddy\Repository\RevisionLog\RevisionLog;
20
21
class PathsPlugin extends AbstractRepositoryCollectorPlugin
22
{
23
24
	const TYPE_NEW = 'new';
25
26
	const TYPE_EXISTING = 'existing';
27
28
	const STATISTIC_PATH_ADDED = 'path_added';
29
30
	const STATISTIC_PATH_FOUND = 'path_found';
31
32
	const STATISTIC_PROJECT_ADDED = 'project_added';
33
34
	const STATISTIC_PROJECT_FOUND = 'project_found';
35
36
	const STATISTIC_PROJECT_COLLISION_FOUND = 'project_collision_found';
37
38
	const STATISTIC_REF_ADDED = 'ref_added';
39
40
	const STATISTIC_REF_FOUND = 'ref_found';
41
42
	const STATISTIC_COMMIT_ADDED_TO_PROJECT = 'commit_added_to_project';
43
44
	const STATISTIC_COMMIT_ADDED_TO_REF = 'commit_added_to_ref';
45
46
	const STATISTIC_EMPTY_COMMIT = 'empty_commit';
47
48
	/**
49
	 * Database cache.
50
	 *
51
	 * @var DatabaseCache
52
	 */
53
	private $_databaseCache;
54
55
	/**
56
	 * Projects.
57
	 *
58
	 * @var array
59
	 */
60
	private $_projects = array(
61
		self::TYPE_NEW => array(),
62
		self::TYPE_EXISTING => array(),
63
	);
64
65
	/**
66
	 * Refs.
67
	 *
68
	 * @var array
69
	 */
70
	private $_refs = array();
71
72
	/**
73
	 * Repository connector.
74
	 *
75
	 * @var Connector
76
	 */
77
	private $_repositoryConnector;
78
79
	/**
80
	 * Path collision detector.
81
	 *
82
	 * @var PathCollisionDetector
83
	 */
84
	private $_pathCollisionDetector;
85
86
	/**
87
	 * Create paths revision log plugin.
88
	 *
89
	 * @param ExtendedPdoInterface  $database                Database.
90
	 * @param RepositoryFiller      $repository_filler       Repository filler.
91
	 * @param DatabaseCache         $database_cache          Database cache.
92
	 * @param Connector             $repository_connector    Repository connector.
93
	 * @param PathCollisionDetector $path_collision_detector Path collision detector.
94
	 */
95 47
	public function __construct(
96
		ExtendedPdoInterface $database,
97
		RepositoryFiller $repository_filler,
98
		DatabaseCache $database_cache,
99
		Connector $repository_connector,
100
		PathCollisionDetector $path_collision_detector
101
	) {
102 47
		parent::__construct($database, $repository_filler);
103
104 47
		$this->_databaseCache = $database_cache;
105 47
		$this->_repositoryConnector = $repository_connector;
106 47
		$this->_pathCollisionDetector = $path_collision_detector;
107
108 47
		$this->initDatabaseCache();
109
	}
110
111
	/**
112
	 * Hook, that is called before "RevisionLog::refresh" method call.
113
	 *
114
	 * @return void
115
	 */
116 47
	public function whenDatabaseReady()
117
	{
118 47
		$sql = 'SELECT Path
119 47
				FROM Projects';
120 47
		$project_paths = $this->database->fetchCol($sql);
121
122 47
		$this->_pathCollisionDetector->addPaths($project_paths);
123
	}
124
125
	/**
126
	 * Initializes database cache.
127
	 *
128
	 * @return void
129
	 */
130 47
	protected function initDatabaseCache()
131
	{
132 47
		$this->_databaseCache->cacheTable('Projects');
133 47
		$this->_databaseCache->cacheTable('ProjectRefs');
134 47
		$this->_databaseCache->cacheTable('Paths');
135
	}
136
137
	/**
138
	 * Returns plugin name.
139
	 *
140
	 * @return string
141
	 */
142 28
	public function getName()
143
	{
144 28
		return 'paths';
145
	}
146
147
	/**
148
	 * Returns revision query flags.
149
	 *
150
	 * @return array
151
	 */
152 1
	public function getRevisionQueryFlags()
153
	{
154 1
		return array(RevisionLog::FLAG_VERBOSE);
155
	}
156
157
	/**
158
	 * Defines parsing statistic types.
159
	 *
160
	 * @return array
161
	 */
162 47
	public function defineStatisticTypes()
163
	{
164 47
		return array(
165 47
			self::STATISTIC_PATH_ADDED,
166 47
			self::STATISTIC_PATH_FOUND,
167 47
			self::STATISTIC_PROJECT_ADDED,
168 47
			self::STATISTIC_PROJECT_FOUND,
169 47
			self::STATISTIC_PROJECT_COLLISION_FOUND,
170 47
			self::STATISTIC_REF_ADDED,
171 47
			self::STATISTIC_REF_FOUND,
172 47
			self::STATISTIC_COMMIT_ADDED_TO_PROJECT,
173 47
			self::STATISTIC_COMMIT_ADDED_TO_REF,
174 47
			self::STATISTIC_EMPTY_COMMIT,
175 47
		);
176
	}
177
178
	/**
179
	 * Does actual parsing.
180
	 *
181
	 * @param integer           $revision  Revision.
182
	 * @param \SimpleXMLElement $log_entry Log Entry.
183
	 *
184
	 * @return void
185
	 */
186 21
	protected function doParse($revision, \SimpleXMLElement $log_entry)
187
	{
188
		// Reset cached info after previous revision processing.
189 21
		$this->_projects = array(
190 21
			self::TYPE_NEW => array(),
191 21
			self::TYPE_EXISTING => array(),
192 21
		);
193 21
		$this->_refs = array();
194
195
		// This is empty revision.
196 21
		if ( !isset($log_entry->paths) ) {
197 1
			$this->recordStatistic(self::STATISTIC_EMPTY_COMMIT);
198
199 1
			return;
200
		}
201
202 20
		foreach ( $this->sortPaths($log_entry->paths) as $path_node ) {
203 20
			$kind = (string)$path_node['kind'];
204 20
			$action = (string)$path_node['action'];
205 20
			$path = $this->adaptPathToKind((string)$path_node, $kind);
206
207 20
			if ( $path_node['copyfrom-rev'] !== null ) {
208 2
				$copy_revision = (int)$path_node['copyfrom-rev'];
209 2
				$copy_path = $this->adaptPathToKind((string)$path_node['copyfrom-path'], $kind);
210 2
				$copy_path_id = $this->processPath($copy_path, $copy_revision, '', false);
211
			}
212
			else {
213 20
				$copy_revision = null;
214 20
				$copy_path_id = null;
215
			}
216
217 20
			$this->repositoryFiller->addPathToCommit(
218 20
				$revision,
219 20
				$action,
220 20
				$kind,
221 20
				$this->processPath($path, $revision, $action),
222 20
				isset($copy_revision) ? $copy_revision : null,
223 20
				isset($copy_path_id) ? $copy_path_id : null
224 20
			);
225
		}
226
227 20
		foreach ( array_keys($this->_projects[self::TYPE_EXISTING]) as $project_id ) {
228 2
			$this->addCommitToProject($revision, $project_id);
229
		}
230
231 20
		foreach ( $this->_projects[self::TYPE_NEW] as $project_id => $project_path ) {
232 6
			$associated_revisions = $this->addMissingCommitsToProject($project_id, $project_path);
233
234 6
			if ( !in_array($revision, $associated_revisions) ) {
235 4
				$this->addCommitToProject($revision, $project_id);
236
			}
237
		}
238
239 20
		foreach ( array_keys($this->_refs) as $ref_id ) {
240 6
			$this->addCommitToRef($revision, $ref_id);
241
		}
242
	}
243
244
	/**
245
	 * @inheritDoc
246
	 *
247
	 * @throws \RuntimeException When attempting to remove plugin collected data.
248
	 */
249
	protected function remove($revision)
250
	{
251
		throw new \RuntimeException('Not supported.');
252
	}
253
254
	/**
255
	 * Sorts paths to move parent folders above their sub-folders.
256
	 *
257
	 * @param \SimpleXMLElement $paths Paths.
258
	 *
259
	 * @return \SimpleXMLElement[]
260
	 */
261 20
	protected function sortPaths(\SimpleXMLElement $paths)
262
	{
263 20
		$sorted_paths = array();
264
265 20
		foreach ( $paths->path as $path_node ) {
266 20
			$sorted_paths[(string)$path_node] = $path_node;
267
		}
268
269 20
		ksort($sorted_paths, defined('SORT_NATURAL') ? SORT_NATURAL : SORT_STRING);
270
271 20
		return $sorted_paths;
272
	}
273
274
	/**
275
	 * Processes path.
276
	 *
277
	 * @param string  $path     Path.
278
	 * @param integer $revision Revision.
279
	 * @param string  $action   Action.
280
	 * @param boolean $is_usage This is usage.
281
	 *
282
	 * @return integer
283
	 */
284 20
	protected function processPath($path, $revision, $action, $is_usage = true)
285
	{
286 20
		$path_hash = $this->repositoryFiller->getPathChecksum($path);
287
288 20
		$sql = 'SELECT Id, ProjectPath, RefName, RevisionAdded, RevisionDeleted, RevisionLastSeen
289
				FROM Paths
290 20
				WHERE PathHash = :path_hash';
291 20
		$path_data = $this->_databaseCache->getFromCache(
292 20
			'Paths',
293 20
			$path_hash,
294 20
			$sql,
295 20
			array('path_hash' => $path_hash)
296 20
		);
297
298 20
		if ( $path_data !== false ) {
299 9
			if ( $action ) {
300 8
				$fields_hash = $this->repositoryFiller->getPathTouchFields($action, $revision, $path_data);
0 ignored issues
show
Bug introduced by
It seems like $path_data can also be of type true; however, parameter $path_data of ConsoleHelpers\SVNBuddy\...r::getPathTouchFields() does only seem to accept array, 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

300
				$fields_hash = $this->repositoryFiller->getPathTouchFields($action, $revision, /** @scrutinizer ignore-type */ $path_data);
Loading history...
301
302 8
				if ( $fields_hash ) {
303 8
					$touched_paths = $this->repositoryFiller->touchPath($path, $revision, $fields_hash);
304
305 8
					foreach ( $touched_paths as $touched_path_hash => $touched_path_fields_hash ) {
306 8
						if ( $this->_databaseCache->getFromCache('Paths', $touched_path_hash) !== false ) {
307 8
							$this->_databaseCache->setIntoCache('Paths', $touched_path_hash, $touched_path_fields_hash);
308
						}
309
					}
310
				}
311
			}
312
313 9
			if ( $path_data['ProjectPath'] && $path_data['RefName'] ) {
314 1
				$project_id = $this->processProject($path_data['ProjectPath'], $is_usage);
315
316
				// There is no project for missing copy source paths.
317 1
				if ( $project_id ) {
318 1
					$this->processRef($project_id, $path_data['RefName'], $is_usage);
319
				}
320
			}
321
322 9
			$this->recordStatistic(self::STATISTIC_PATH_FOUND);
323
324 9
			return $path_data['Id'];
325
		}
326
327 20
		$ref = $this->_repositoryConnector->getRefByPath($path);
328
329 20
		if ( $ref !== false ) {
330 7
			$project_path = substr($path, 0, strpos($path, $ref));
0 ignored issues
show
Bug introduced by
It seems like $ref can also be of type true; however, parameter $needle of strpos() does only seem to accept string, 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

330
			$project_path = substr($path, 0, strpos($path, /** @scrutinizer ignore-type */ $ref));
Loading history...
331
332 7
			if ( $this->_pathCollisionDetector->isCollision($project_path) ) {
333 2
				$project_path = $ref = '';
334 7
				$this->recordStatistic(self::STATISTIC_PROJECT_COLLISION_FOUND);
335
			}
336
		}
337
		else {
338 15
			$project_path = '';
339
		}
340
341 20
		$path_id = $this->repositoryFiller->addPath($path, (string)$ref, $project_path, $revision);
342 20
		$this->_databaseCache->setIntoCache('Paths', $path_hash, array(
343 20
			'Id' => $path_id,
344 20
			'ProjectPath' => $project_path,
345 20
			'RefName' => (string)$ref,
346 20
			'RevisionAdded' => $revision,
347 20
			'RevisionDeleted' => null,
348 20
			'RevisionLastSeen' => $revision,
349 20
		));
350
351 20
		if ( $project_path && $ref ) {
352 6
			$project_id = $this->processProject($project_path, $is_usage);
353
354
			// There is no project for missing copy source paths.
355 6
			if ( $project_id ) {
356 6
				$this->processRef($project_id, $ref, $is_usage);
0 ignored issues
show
Bug introduced by
It seems like $ref can also be of type true; however, parameter $ref of ConsoleHelpers\SVNBuddy\...thsPlugin::processRef() does only seem to accept string, 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

356
				$this->processRef($project_id, /** @scrutinizer ignore-type */ $ref, $is_usage);
Loading history...
357
			}
358
		}
359
360 20
		$this->recordStatistic(self::STATISTIC_PATH_ADDED);
361
362 20
		return $path_id;
363
	}
364
365
	/**
366
	 * Adapts path to kind.
367
	 *
368
	 * @param string $path Path.
369
	 * @param string $kind Kind.
370
	 *
371
	 * @return string
372
	 */
373 20
	protected function adaptPathToKind($path, $kind)
374
	{
375 20
		if ( $kind === 'dir' ) {
376 13
			$path .= '/';
377
		}
378
379 20
		return $path;
380
	}
381
382
	/**
383
	 * Processes project.
384
	 *
385
	 * @param string  $project_path Project path.
386
	 * @param boolean $is_usage     This is usage.
387
	 *
388
	 * @return integer
389
	 */
390 6
	protected function processProject($project_path, $is_usage = true)
391
	{
392 6
		$sql = 'SELECT Id
393
				FROM Projects
394 6
				WHERE Path = :path';
395 6
		$project_data = $this->_databaseCache->getFromCache(
396 6
			'Projects',
397 6
			$project_path,
398 6
			$sql,
399 6
			array('path' => $project_path)
400 6
		);
401
402 6
		if ( $project_data !== false ) {
403 3
			$project_id = $project_data['Id'];
404
405
			// Don't consider project both new & existing (e.g. when single commit adds several branches).
406 3
			if ( $is_usage && !isset($this->_projects[self::TYPE_NEW][$project_id]) ) {
407 2
				$this->_projects[self::TYPE_EXISTING][$project_id] = $project_path;
408 2
				$this->recordStatistic(self::STATISTIC_PROJECT_FOUND);
409
			}
410
411 3
			return $project_id;
412
		}
413
414
		// Ignore fact, that project of non-used (copied) path doesn't exist.
415 6
		if ( !$is_usage ) {
416 1
			return null;
417
		}
418
419 6
		$project_id = $this->repositoryFiller->addProject($project_path);
420 6
		$this->_databaseCache->setIntoCache('Projects', $project_path, array('Id' => $project_id));
421 6
		$this->_pathCollisionDetector->addPaths(array($project_path));
422
423 6
		if ( $is_usage ) {
0 ignored issues
show
introduced by
The condition $is_usage is always true.
Loading history...
424 6
			$this->_projects[self::TYPE_NEW][$project_id] = $project_path;
425 6
			$this->recordStatistic(self::STATISTIC_PROJECT_ADDED);
426
		}
427
428 6
		return $project_id;
429
	}
430
431
	/**
432
	 * Processes ref.
433
	 *
434
	 * @param integer $project_id Project ID.
435
	 * @param string  $ref        Ref.
436
	 * @param boolean $is_usage   This is usage.
437
	 *
438
	 * @return integer
439
	 */
440 6
	protected function processRef($project_id, $ref, $is_usage = true)
441
	{
442 6
		$cache_key = $project_id . ':' . $ref;
443
444 6
		$sql = 'SELECT Id
445
				FROM ProjectRefs
446 6
				WHERE ProjectId = :project_id AND Name = :ref';
447 6
		$ref_data = $this->_databaseCache->getFromCache(
448 6
			'ProjectRefs',
449 6
			$cache_key,
450 6
			$sql,
451 6
			array('project_id' => $project_id, 'ref' => $ref)
452 6
		);
453
454 6
		if ( $ref_data !== false ) {
455 2
			$ref_id = $ref_data['Id'];
456
457 2
			if ( $is_usage ) {
458 2
				$this->_refs[$ref_id] = true;
459
			}
460
461 2
			$this->recordStatistic(self::STATISTIC_REF_FOUND);
462
463 2
			return $ref_id;
464
		}
465
466 6
		$ref_id = $this->repositoryFiller->addRefToProject($ref, $project_id);
467 6
		$this->_databaseCache->setIntoCache('ProjectRefs', $cache_key, array('Id' => $ref_id));
468
469 6
		if ( $is_usage ) {
470 6
			$this->_refs[$ref_id] = true;
471
		}
472
473 6
		$this->recordStatistic(self::STATISTIC_REF_ADDED);
474
475 6
		return $ref_id;
476
	}
477
478
	/**
479
	 * Retroactively map paths/commits to project, where path doesn't contain ref.
480
	 *
481
	 * @param integer $project_id   Project ID.
482
	 * @param string  $project_path Project path.
483
	 *
484
	 * @return array
485
	 */
486 6
	protected function addMissingCommitsToProject($project_id, $project_path)
487
	{
488 6
		$sql = "SELECT Id
489
				FROM Paths
490 6
				WHERE ProjectPath = '' AND Path LIKE :path_matcher";
491 6
		$paths_without_project = $this->database->fetchCol($sql, array('path_matcher' => $project_path . '%'));
492
493 6
		if ( !$paths_without_project ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $paths_without_project of type array 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...
494 4
			return array();
495
		}
496
497 2
		$this->repositoryFiller->movePathsIntoProject($paths_without_project, $project_path);
498
499 2
		$sql = 'SELECT Revision
500
				FROM CommitPaths
501 2
				WHERE PathId IN (:path_ids)';
502 2
		$commit_revisions = $this->database->fetchCol($sql, array('path_ids' => $paths_without_project));
503
504 2
		foreach ( array_unique($commit_revisions) as $commit_revision ) {
505 2
			$this->addCommitToProject($commit_revision, $project_id);
506
		}
507
508 2
		return $commit_revisions;
509
	}
510
511
	/**
512
	 * Associates revision with project.
513
	 *
514
	 * @param integer $revision   Revision.
515
	 * @param integer $project_id Project.
516
	 *
517
	 * @return void
518
	 */
519 6
	protected function addCommitToProject($revision, $project_id)
520
	{
521 6
		$this->repositoryFiller->addCommitToProject($revision, $project_id);
522 6
		$this->recordStatistic(self::STATISTIC_COMMIT_ADDED_TO_PROJECT);
523
	}
524
525
	/**
526
	 * Associates revision with ref.
527
	 *
528
	 * @param integer $revision Revision.
529
	 * @param integer $ref_id   Ref.
530
	 *
531
	 * @return void
532
	 */
533 6
	protected function addCommitToRef($revision, $ref_id)
534
	{
535 6
		$this->repositoryFiller->addCommitToRef($revision, $ref_id);
536 6
		$this->recordStatistic(self::STATISTIC_COMMIT_ADDED_TO_REF);
537
	}
538
539
	/**
540
	 * Find revisions by collected data.
541
	 *
542
	 * @param array       $criteria     Criteria.
543
	 * @param string|null $project_path Project path.
544
	 *
545
	 * @return array
546
	 */
547 18
	public function find(array $criteria, $project_path)
548
	{
549 18
		if ( !$criteria ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $criteria of type array 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...
550 1
			return array();
551
		}
552
553 17
		$project_id = $this->getProject($project_path);
554
555 16
		if ( reset($criteria) === '' ) {
556
			// Include revisions from all paths.
557 1
			$path_revisions = $this->findAllRevisions($project_id);
558
		}
559
		else {
560
			// Include revisions from given sub-path only.
561 15
			$path_revisions = array();
562
563 15
			foreach ( $criteria as $criterion ) {
564 15
				if ( strpos($criterion, ':') === false ) {
565
					// Guess $field based on path itself.
566 7
					$field = substr($criterion, -1, 1) === '/' ? 'sub-match' : 'exact';
567 7
					$value = $criterion;
568
				}
569
				else {
570
					// Use $field, that is given.
571 8
					list ($field, $value) = explode(':', $criterion, 2);
572
				}
573
574 15
				$tmp_revisions = $this->processFieldCriterion($project_id, $field, $value);
575
576 14
				foreach ( $tmp_revisions as $revision ) {
577 9
					$path_revisions[$revision] = true;
578
				}
579
			}
580
		}
581
582 15
		$path_revisions = array_keys($path_revisions);
583 15
		sort($path_revisions, SORT_NUMERIC);
584
585 15
		return $path_revisions;
586
	}
587
588
	/**
589
	 * Finds all revisions in a project.
590
	 *
591
	 * @param integer $project_id Project ID.
592
	 *
593
	 * @return array
594
	 */
595 1
	protected function findAllRevisions($project_id)
596
	{
597
		// Same as getting all revisions from "projects" plugin.
598 1
		$sql = 'SELECT Revision
599
				FROM CommitProjects
600 1
				WHERE ProjectId = :project_id';
601
602 1
		return array_flip($this->database->fetchCol($sql, array('project_id' => $project_id)));
603
	}
604
605
	/**
606
	 * Processes search request, when field is specified in criterion.
607
	 *
608
	 * @param integer $project_id Project ID.
609
	 * @param string  $field      Field.
610
	 * @param mixed   $value      Value.
611
	 *
612
	 * @return array
613
	 * @throws \InvalidArgumentException When non-supported search field is given.
614
	 */
615 15
	protected function processFieldCriterion($project_id, $field, $value)
616
	{
617 15
		if ( $field === 'action' ) {
618 2
			return $this->findByAction($project_id, $value);
619
		}
620
621 14
		if ( $field === 'kind' ) {
622 1
			return $this->findByKind($project_id, $value);
623
		}
624
625 13
		if ( $field === 'exact' ) {
626 5
			return $this->findByExactMatch($project_id, $value);
627
		}
628
629 8
		if ( $field === 'sub-match' ) {
630 7
			return $this->findBySubMatch($project_id, $value);
631
		}
632
633 1
		$error_msg = 'Searching by "%s" is not supported by "%s" plugin.';
634 1
		throw new \InvalidArgumentException(sprintf($error_msg, $field, $this->getName()));
635
	}
636
637
	/**
638
	 * Finds revisions by action.
639
	 *
640
	 * @param integer $project_id Project id.
641
	 * @param string  $action     Action.
642
	 *
643
	 * @return array
644
	 */
645 2
	protected function findByAction($project_id, $action)
646
	{
647 2
		$sql = 'SELECT DISTINCT cpr.Revision
648
				FROM CommitPaths cpa
649
				JOIN CommitProjects cpr ON cpr.Revision = cpa.Revision
650 2
				WHERE cpr.ProjectId = :project_id AND cpa.Action LIKE :action';
651 2
		$tmp_revisions = $this->database->fetchCol($sql, array(
652 2
			'project_id' => $project_id,
653 2
			'action' => $action,
654 2
		));
655
656 2
		return $tmp_revisions;
657
	}
658
659
	/**
660
	 * Finds revisions by kind.
661
	 *
662
	 * @param integer $project_id Project ID.
663
	 * @param string  $kind       Kind.
664
	 *
665
	 * @return array
666
	 */
667 1
	protected function findByKind($project_id, $kind)
668
	{
669 1
		$sql = 'SELECT DISTINCT cpr.Revision
670
				FROM CommitPaths cpa
671
				JOIN CommitProjects cpr ON cpr.Revision = cpa.Revision
672 1
				WHERE cpr.ProjectId = :project_id AND cpa.Kind LIKE :kind';
673 1
		$tmp_revisions = $this->database->fetchCol($sql, array(
674 1
			'project_id' => $project_id,
675 1
			'kind' => $kind,
676 1
		));
677
678 1
		return $tmp_revisions;
679
	}
680
681
	/**
682
	 * Finds revisions by sub-match.
683
	 *
684
	 * @param integer      $project_id   Project ID.
685
	 * @param string       $path         Path.
686
	 * @param integer|null $max_revision Max revision.
687
	 *
688
	 * @return array
689
	 */
690 7
	protected function findBySubMatch($project_id, $path, $max_revision = null)
691
	{
692 7
		$path_id = $this->getPathId($path);
693
694 7
		if ( $path_id === false ) {
0 ignored issues
show
introduced by
The condition $path_id === false is always false.
Loading history...
695 3
			return array();
696
		}
697
698 4
		$copy_data = $this->getPathCopyData($path_id, $max_revision);
699
700 4
		if ( $this->_repositoryConnector->isRefRoot($path) ) {
701 2
			$where_clause = array(
702 2
				'RefId = :ref_id',
703 2
			);
704
705 2
			$bind_params = array(
706 2
				'ref_id' => $this->getRefId($project_id, $this->_repositoryConnector->getRefByPath($path)),
707 2
			);
708
709
			// Revisions, made after copy revision.
710 2
			if ( $copy_data ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $copy_data of type array 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...
711 2
				$where_clause[] = 'Revision >= :min_revision';
712 2
				$bind_params['min_revision'] = $copy_data['Revision'];
713
			}
714
715
			// Revisions made before copy revision.
716 2
			if ( isset($max_revision) ) {
717 2
				$where_clause[] = 'Revision < :max_revision';
718 2
				$bind_params['max_revision'] = $max_revision;
719
			}
720
721 2
			$sql = 'SELECT DISTINCT Revision
722
					FROM CommitRefs
723 2
					WHERE (' . implode(') AND (', $where_clause) . ')';
724 2
			$results = $this->database->fetchCol($sql, $bind_params);
725
		}
726
		else {
727 2
			$where_clause = array(
728 2
				'cpr.ProjectId = :project_id',
729 2
				'p.Path LIKE :path',
730 2
			);
731
732 2
			$bind_params = array(
733 2
				'project_id' => $project_id,
734 2
				'path' => $path . '%',
735 2
			);
736
737
			// Revisions, made after copy revision.
738 2
			if ( $copy_data ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $copy_data of type array 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...
739 2
				$where_clause[] = 'cpr.Revision >= :min_revision';
740 2
				$bind_params['min_revision'] = $copy_data['Revision'];
741
			}
742
743
			// Revisions made before copy revision.
744 2
			if ( isset($max_revision) ) {
745 2
				$where_clause[] = 'cpr.Revision < :max_revision';
746 2
				$bind_params['max_revision'] = $max_revision;
747
			}
748
749 2
			$sql = 'SELECT DISTINCT cpr.Revision
750
					FROM CommitProjects cpr
751
					JOIN CommitPaths cpa ON cpa.Revision = cpr.Revision
752
					JOIN Paths p ON p.Id = cpa.PathId
753 2
					WHERE (' . implode(') AND (', $where_clause) . ')';
754 2
			$results = $this->database->fetchCol($sql, $bind_params);
755
		}
756
757 4
		if ( !$copy_data ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $copy_data of type array 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...
758 4
			return $results;
759
		}
760
761 4
		return array_merge(
762 4
			$results,
763 4
			$this->findBySubMatch($project_id, $this->getPathFromId($copy_data['CopyPathId']), $copy_data['Revision'])
764 4
		);
765
	}
766
767
	/**
768
	 * Returns ref ID.
769
	 *
770
	 * @param integer $project_id Project ID.
771
	 * @param string  $ref_name   Ref name.
772
	 *
773
	 * @return integer
774
	 */
775 2
	protected function getRefId($project_id, $ref_name)
776
	{
777 2
		$sql = 'SELECT Id
778
				FROM ProjectRefs
779 2
				WHERE ProjectId = :project_id AND Name = :ref_name';
780
781 2
		return $this->database->fetchValue($sql, array(
782 2
			'project_id' => $project_id,
783 2
			'ref_name' => $ref_name,
784 2
		));
785
	}
786
787
	/**
788
	 * Finds revisions by exact match.
789
	 *
790
	 * @param integer      $project_id   Project ID.
791
	 * @param string       $path         Path.
792
	 * @param integer|null $max_revision Max revision.
793
	 *
794
	 * @return array
795
	 */
796 5
	protected function findByExactMatch($project_id, $path, $max_revision = null)
797
	{
798 5
		$path_id = $this->getPathId($path);
799
800 5
		if ( $path_id === false ) {
0 ignored issues
show
introduced by
The condition $path_id === false is always false.
Loading history...
801 2
			return array();
802
		}
803
804 3
		$copy_data = $this->getPathCopyData($path_id, $max_revision);
805
806 3
		$where_clause = array(
807 3
			'cpr.ProjectId = :project_id',
808 3
			'p.PathHash = :path_hash',
809 3
		);
810
811 3
		$bind_params = array(
812 3
			'project_id' => $project_id,
813 3
			'path_hash' => $this->repositoryFiller->getPathChecksum($path),
814 3
		);
815
816
		// Revisions, made after copy revision.
817 3
		if ( $copy_data ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $copy_data of type array 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...
818 2
			$where_clause[] = 'cpr.Revision >= :min_revision';
819 2
			$bind_params['min_revision'] = $copy_data['Revision'];
820
		}
821
822
		// Revisions made before copy revision.
823 3
		if ( isset($max_revision) ) {
824 2
			$where_clause[] = 'cpr.Revision < :max_revision';
825 2
			$bind_params['max_revision'] = $max_revision;
826
		}
827
828 3
		$sql = 'SELECT DISTINCT cpr.Revision
829
				FROM CommitProjects cpr
830
				JOIN CommitPaths cpa ON cpa.Revision = cpr.Revision
831
				JOIN Paths p ON p.Id = cpa.PathId
832 3
				WHERE (' . implode(') AND (', $where_clause) . ')';
833 3
		$results = $this->database->fetchCol($sql, $bind_params);
834
835 3
		if ( !$copy_data ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $copy_data of type array 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...
836 3
			return $results;
837
		}
838
839 2
		return array_merge(
840 2
			$results,
841 2
			$this->findByExactMatch($project_id, $this->getPathFromId($copy_data['CopyPathId']), $copy_data['Revision'])
842 2
		);
843
	}
844
845
	/**
846
	 * Returns path copy data.
847
	 *
848
	 * @param integer      $path_id      Path ID.
849
	 * @param integer|null $max_revision Max revision.
850
	 *
851
	 * @return array
852
	 */
853 7
	protected function getPathCopyData($path_id, $max_revision = null)
854
	{
855 7
		$where_clause = array(
856 7
			'PathId = :path_id',
857 7
			'CopyPathId IS NOT NULL',
858 7
		);
859
860 7
		$bind_params = array(
861 7
			'path_id' => $path_id,
862 7
		);
863
864
		// Copy made before, maximal revision.
865 7
		if ( isset($max_revision) ) {
866 6
			$where_clause[] = 'Revision <= :max_revision';
867 6
			$bind_params['max_revision'] = $max_revision;
868
		}
869
870 7
		$sql = 'SELECT Revision, CopyPathId
871
				FROM CommitPaths
872 7
				WHERE (' . implode(') AND (', $where_clause) . ')
873
				ORDER BY Revision DESC
874 7
				LIMIT 1';
875
876 7
		return $this->database->fetchOne($sql, $bind_params);
877
	}
878
879
	/**
880
	 * Returns path id.
881
	 *
882
	 * @param string $path Path.
883
	 *
884
	 * @return integer
885
	 */
886 12
	protected function getPathId($path)
887
	{
888 12
		$sql = 'SELECT Id
889
				FROM Paths
890 12
				WHERE PathHash = :path_hash';
891
892 12
		return $this->database->fetchValue($sql, array(
893 12
			'path_hash' => $this->repositoryFiller->getPathChecksum($path),
894 12
		));
895
	}
896
897
	/**
898
	 * Returns path by id.
899
	 *
900
	 * @param integer $path_id Path ID.
901
	 *
902
	 * @return string
903
	 */
904 6
	protected function getPathFromId($path_id)
905
	{
906 6
		$sql = 'SELECT Path
907
				FROM Paths
908 6
				WHERE Id = :path_id';
909
910 6
		return $this->database->fetchValue($sql, array(
911 6
			'path_id' => $path_id,
912 6
		));
913
	}
914
915
	/**
916
	 * Returns information about revisions.
917
	 *
918
	 * @param array $revisions Revisions.
919
	 *
920
	 * @return array
921
	 */
922 2
	public function getRevisionsData(array $revisions)
923
	{
924 2
		$results = array();
925
926 2
		$sql = 'SELECT cp.Revision, p1.Path, cp.Kind, cp.Action, p2.Path AS CopyPath, cp.CopyRevision
927
				FROM CommitPaths cp
928
				JOIN Paths p1 ON p1.Id = cp.PathId
929
				LEFT JOIN Paths p2 ON p2.Id = cp.CopyPathId
930 2
				WHERE cp.Revision IN (:revision_ids)';
931 2
		$revisions_data = $this->getRawRevisionsData($sql, 'revision_ids', $revisions);
932
933 2
		foreach ( $revisions_data as $revision_data ) {
934 1
			$revision = $revision_data['Revision'];
935
936 1
			if ( !isset($results[$revision]) ) {
937 1
				$results[$revision] = array();
938
			}
939
940 1
			$results[$revision][] = array(
941 1
				'path' => $revision_data['Path'],
942 1
				'kind' => $revision_data['Kind'],
943 1
				'action' => $revision_data['Action'],
944 1
				'copyfrom-path' => $revision_data['CopyPath'],
945 1
				'copyfrom-rev' => $revision_data['CopyRevision'],
946 1
			);
947
		}
948
949 2
		$this->assertNoMissingRevisions($revisions, $results);
950
951 1
		return $results;
952
	}
953
954
	/**
955
	 * Frees consumed memory.
956
	 *
957
	 * @return void
958
	 *
959
	 * @codeCoverageIgnore
960
	 */
961
	protected function freeMemoryManually()
962
	{
963
		parent::freeMemoryManually();
964
965
		$this->_databaseCache->clear();
966
	}
967
968
}
969