Failed Conditions
Push — master ( 9ab24a...f8fe4f )
by Alexander
03:47
created

PathsPlugin::findAllRevisions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
ccs 3
cts 3
cp 1
rs 9.6666
cc 1
eloc 3
nc 1
nop 1
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\Plugin;
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 46
	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 46
		parent::__construct($database, $repository_filler);
103
104 46
		$this->_databaseCache = $database_cache;
105 46
		$this->_repositoryConnector = $repository_connector;
106 46
		$this->_pathCollisionDetector = $path_collision_detector;
107
108 46
		$this->initDatabaseCache();
109 46
	}
110
111
	/**
112
	 * Hook, that is called before "RevisionLog::refresh" method call.
113
	 *
114
	 * @return void
115
	 */
116 46
	public function whenDatabaseReady()
117
	{
118
		$sql = 'SELECT Path
119 46
				FROM Projects';
120 46
		$project_paths = $this->database->fetchCol($sql);
121
122 46
		$this->_pathCollisionDetector->addPaths($project_paths);
123 46
	}
124
125
	/**
126
	 * Initializes database cache.
127
	 *
128
	 * @return void
129
	 */
130 46
	protected function initDatabaseCache()
131
	{
132 46
		$this->_databaseCache->cacheTable('Projects');
133 46
		$this->_databaseCache->cacheTable('ProjectRefs');
134 46
		$this->_databaseCache->cacheTable('Paths');
135 46
	}
136
137
	/**
138
	 * Returns plugin name.
139
	 *
140
	 * @return string
141
	 */
142 27
	public function getName()
143
	{
144 27
		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 46
	public function defineStatisticTypes()
163
	{
164
		return array(
165 46
			self::STATISTIC_PATH_ADDED,
166 46
			self::STATISTIC_PATH_FOUND,
167 46
			self::STATISTIC_PROJECT_ADDED,
168 46
			self::STATISTIC_PROJECT_FOUND,
169 46
			self::STATISTIC_PROJECT_COLLISION_FOUND,
170 46
			self::STATISTIC_REF_ADDED,
171 46
			self::STATISTIC_REF_FOUND,
172 46
			self::STATISTIC_COMMIT_ADDED_TO_PROJECT,
173 46
			self::STATISTIC_COMMIT_ADDED_TO_REF,
174 46
			self::STATISTIC_EMPTY_COMMIT,
175 46
		);
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 20
	protected function doParse($revision, \SimpleXMLElement $log_entry)
187
	{
188
		// Reset cached info after previous revision processing.
189 20
		$this->_projects = array(
190 20
			self::TYPE_NEW => array(),
191 20
			self::TYPE_EXISTING => array(),
192
		);
193 20
		$this->_refs = array();
194
195
		// This is empty revision.
196 20
		if ( !isset($log_entry->paths) ) {
197 1
			$this->recordStatistic(self::STATISTIC_EMPTY_COMMIT);
198
199 1
			return;
200
		}
201
202 19
		foreach ( $this->sortPaths($log_entry->paths) as $path_node ) {
203 19
			$kind = (string)$path_node['kind'];
204 19
			$action = (string)$path_node['action'];
205 19
			$path = $this->adaptPathToKind((string)$path_node, $kind);
206
207 19
			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 2
			}
212
			else {
213 19
				$copy_revision = null;
214 19
				$copy_path_id = null;
215
			}
216
217 19
			$this->repositoryFiller->addPathToCommit(
218 19
				$revision,
219 19
				$action,
220 19
				$kind,
221 19
				$this->processPath($path, $revision, $action),
222 19
				isset($copy_revision) ? $copy_revision : null,
223 19
				isset($copy_path_id) ? $copy_path_id : null
224 19
			);
225 19
		}
226
227 19
		foreach ( array_keys($this->_projects[self::TYPE_EXISTING]) as $project_id ) {
228 2
			$this->addCommitToProject($revision, $project_id);
229 19
		}
230
231 19
		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 4
			}
237 19
		}
238
239 19
		foreach ( array_keys($this->_refs) as $ref_id ) {
240 6
			$this->addCommitToRef($revision, $ref_id);
241 19
		}
242 19
	}
243
244
	/**
245
	 * Sorts paths to move parent folders above their sub-folders.
246
	 *
247
	 * @param \SimpleXMLElement $paths Paths.
248
	 *
249
	 * @return \SimpleXMLElement[]
250
	 */
251 19
	protected function sortPaths(\SimpleXMLElement $paths)
252
	{
253 19
		$sorted_paths = array();
254
255 19
		foreach ( $paths->path as $path_node ) {
256 19
			$sorted_paths[(string)$path_node] = $path_node;
257 19
		}
258
259 19
		ksort($sorted_paths, defined('SORT_NATURAL') ? SORT_NATURAL : SORT_STRING);
260
261 19
		return $sorted_paths;
262
	}
263
264
	/**
265
	 * Processes path.
266
	 *
267
	 * @param string  $path     Path.
268
	 * @param integer $revision Revision.
269
	 * @param string  $action   Action.
270
	 * @param boolean $is_usage This is usage.
271
	 *
272
	 * @return integer
273
	 */
274 19
	protected function processPath($path, $revision, $action, $is_usage = true)
275
	{
276 19
		$path_hash = $this->repositoryFiller->getPathChecksum($path);
277
278
		$sql = 'SELECT Id, ProjectPath, RefName, RevisionAdded, RevisionDeleted, RevisionLastSeen
279
				FROM Paths
280 19
				WHERE PathHash = :path_hash';
281 19
		$path_data = $this->_databaseCache->getFromCache(
282 19
			'Paths',
283 19
			$path_hash,
284 19
			$sql,
285 19
			array('path_hash' => $path_hash)
286 19
		);
287
288 19
		if ( $path_data !== false ) {
289 8
			if ( $action ) {
290 7
				$fields_hash = $this->repositoryFiller->getPathTouchFields($action, $revision, $path_data);
0 ignored issues
show
Bug introduced by
It seems like $path_data defined by $this->_databaseCache->g...h_hash' => $path_hash)) on line 281 can also be of type boolean; however, ConsoleHelpers\SVNBuddy\...r::getPathTouchFields() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

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