Passed
Push — master ( 4f2edc...49e198 )
by Alexander
03:14 queued 01:17
created

Connector::_getSvnInfoEntry()   B

Complexity

Conditions 7
Paths 32

Size

Total Lines 38
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 7.0368

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 17
dl 0
loc 38
ccs 20
cts 22
cp 0.9091
rs 8.8333
c 2
b 0
f 0
cc 7
nc 32
nop 2
crap 7.0368
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\Connector;
12
13
14
use ConsoleHelpers\ConsoleKit\ConsoleIO;
15
use ConsoleHelpers\SVNBuddy\Cache\CacheManager;
16
use ConsoleHelpers\ConsoleKit\Config\ConfigEditor;
17
use ConsoleHelpers\SVNBuddy\Exception\RepositoryCommandException;
18
use ConsoleHelpers\SVNBuddy\Process\IProcessFactory;
19
use ConsoleHelpers\SVNBuddy\Repository\Parser\RevisionListParser;
20
21
/**
22
 * Executes command on the repository.
23
 */
24
class Connector
25
{
26
27
	const STATUS_NORMAL = 'normal';
28
29
	const STATUS_ADDED = 'added';
30
31
	const STATUS_CONFLICTED = 'conflicted';
32
33
	const STATUS_UNVERSIONED = 'unversioned';
34
35
	const STATUS_EXTERNAL = 'external';
36
37
	const STATUS_MISSING = 'missing';
38
39
	const STATUS_NONE = 'none';
40
41
	const URL_REGEXP = '#([\w]*)://([^/@\s\']+@)?([^/@:\s\']+)(:\d+)?([^@\s\']*)?#';
42
43
	const SVN_INFO_CACHE_DURATION = '1 year';
44
45
	const SVN_CAT_CACHE_DURATION = '1 month';
46
47
	/**
48
	 * Command factory.
49
	 *
50
	 * @var CommandFactory
51
	 */
52
	private $_commandFactory;
53
54
	/**
55
	 * Console IO.
56
	 *
57
	 * @var ConsoleIO
58
	 */
59
	private $_io;
60
61
	/**
62
	 * Revision list parser.
63
	 *
64
	 * @var RevisionListParser
65
	 */
66
	private $_revisionListParser;
67
68
	/**
69
	 * Cache duration for next invoked command.
70
	 *
71
	 * @var mixed
72
	 */
73
	private $_nextCommandCacheDuration = null;
74
75
	/**
76
	 * Cache overwrite for next invoked command.
77
	 *
78
	 * @var mixed
79
	 */
80
	private $_nextCommandCacheOverwrite = null;
81
82
	/**
83
	 * Whatever to cache last repository revision or not.
84
	 *
85
	 * @var mixed
86
	 */
87
	private $_lastRevisionCacheDuration = null;
88
89
	/**
90
	 * Creates repository connector.
91
	 *
92
	 * @param ConfigEditor       $config_editor        ConfigEditor.
93
	 * @param CommandFactory     $command_factory      Command factory.
94
	 * @param ConsoleIO          $io                   Console IO.
95
	 * @param RevisionListParser $revision_list_parser Revision list parser.
96
	 */
97 97
	public function __construct(
98
		ConfigEditor $config_editor,
99
		CommandFactory $command_factory,
100
		ConsoleIO $io,
101
		RevisionListParser $revision_list_parser
102
	) {
103 97
		$this->_commandFactory = $command_factory;
104 97
		$this->_io = $io;
105 97
		$this->_revisionListParser = $revision_list_parser;
106
107 97
		$cache_duration = $config_editor->get('repository-connector.last-revision-cache-duration');
108
109 97
		if ( (string)$cache_duration === '' || substr($cache_duration, 0, 1) === '0' ) {
0 ignored issues
show
Bug introduced by
It seems like $cache_duration can also be of type array and array and null; however, parameter $string of substr() 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

109
		if ( (string)$cache_duration === '' || substr(/** @scrutinizer ignore-type */ $cache_duration, 0, 1) === '0' ) {
Loading history...
110 4
			$cache_duration = 0;
111 4
		}
112
113 97
		$this->_lastRevisionCacheDuration = $cache_duration;
114 97
	}
115
116
	/**
117
	 * Builds a command.
118
	 *
119
	 * @param string $sub_command Sub command.
120
	 * @param array  $arguments   Arguments.
121
	 *
122
	 * @return Command
123
	 */
124 51
	public function getCommand($sub_command, array $arguments = array())
125
	{
126 51
		$command = $this->_commandFactory->getCommand($sub_command, $arguments);
127
128 51
		if ( isset($this->_nextCommandCacheDuration) ) {
129 27
			$command->setCacheDuration($this->_nextCommandCacheDuration);
130 27
			$this->_nextCommandCacheDuration = null;
131 27
		}
132
133 51
		if ( isset($this->_nextCommandCacheOverwrite) ) {
134 4
			$command->setCacheOverwrite($this->_nextCommandCacheOverwrite);
135 4
			$this->_nextCommandCacheOverwrite = null;
136 4
		}
137
138 51
		return $command;
139
	}
140
141
	/**
142
	 * Sets cache configuration for next created command.
143
	 *
144
	 * @param mixed        $cache_duration  Cache duration.
145
	 * @param boolean|null $cache_overwrite Cache overwrite.
146
	 *
147
	 * @return self
148
	 */
149 31
	public function withCache($cache_duration, $cache_overwrite = null)
150
	{
151 31
		$this->_nextCommandCacheDuration = $cache_duration;
152 31
		$this->_nextCommandCacheOverwrite = $cache_overwrite;
153
154 31
		return $this;
155
	}
156
157
	/**
158
	 * Returns property value.
159
	 *
160
	 * @param string $name        Property name.
161
	 * @param string $path_or_url Path to get property from.
162
	 * @param mixed  $revision    Revision.
163
	 *
164
	 * @return string
165
	 * @throws RepositoryCommandException When other, then missing property exception happens.
166
	 */
167 8
	public function getProperty($name, $path_or_url, $revision = null)
168
	{
169 8
		$arguments = array($name, $path_or_url);
170
171 8
		if ( isset($revision) ) {
172 7
			$arguments[] = '--revision';
173 7
			$arguments[] = $revision;
174 7
		}
175
176
		// The "null" for non-existing properties is never returned, because output is converted to string.
177 8
		$property_value = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $property_value is dead and can be removed.
Loading history...
178
179
		try {
180 8
			$property_value = $this->getCommand('propget', $arguments)->run();
181
		}
182 8
		catch ( RepositoryCommandException $e ) {
183
			// Preserve SVN 1.8- behavior, where reading value of non-existing property returned an empty string.
184 1
			if ( $e->getCode() !== RepositoryCommandException::SVN_ERR_BASE ) {
185
				throw $e;
186
			}
187
		}
188
189 8
		return $property_value;
190
	}
191
192
	/**
193
	 * Returns relative path of given path/url to the root of the repository.
194
	 *
195
	 * @param string $path_or_url Path or url.
196
	 *
197
	 * @return string
198
	 */
199 3
	public function getRelativePath($path_or_url)
200
	{
201 3
		$svn_info_entry = $this->_getSvnInfoEntry($path_or_url, self::SVN_INFO_CACHE_DURATION);
202
203 3
		return preg_replace(
204 3
			'/^' . preg_quote($svn_info_entry->repository->root, '/') . '/',
205 3
			'',
206 3
			(string)$svn_info_entry->url,
207
			1
208 3
		);
209
	}
210
211
	/**
212
	 * Returns repository root url from given path/url.
213
	 *
214
	 * @param string $path_or_url Path or url.
215
	 *
216
	 * @return string
217
	 */
218 3
	public function getRootUrl($path_or_url)
219
	{
220 3
		return (string)$this->_getSvnInfoEntry($path_or_url, self::SVN_INFO_CACHE_DURATION)->repository->root;
221
	}
222
223
	/**
224
	 * Determines if path is a root of the ref.
225
	 *
226
	 * @param string $path Path to a file.
227
	 *
228
	 * @return boolean
229
	 */
230 13
	public function isRefRoot($path)
231
	{
232 13
		$ref = $this->getRefByPath($path);
233
234 13
		if ( $ref === false ) {
235 4
			return false;
236
		}
237
238 9
		return preg_match('#/' . preg_quote($ref, '#') . '/$#', $path) > 0;
0 ignored issues
show
Bug introduced by
It seems like $ref can also be of type true; however, parameter $str of preg_quote() 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

238
		return preg_match('#/' . preg_quote(/** @scrutinizer ignore-type */ $ref, '#') . '/$#', $path) > 0;
Loading history...
239
	}
240
241
	/**
242
	 * Detects ref from given path.
243
	 *
244
	 * @param string $path Path to a file.
245
	 *
246
	 * @return string|boolean
247
	 * @see    getProjectUrl
248
	 */
249 22
	public function getRefByPath($path)
250
	{
251 22
		if ( preg_match('#^.*?/(trunk|branches/[^/]+|tags/[^/]+|releases/[^/]+).*$#', $path, $regs) ) {
252 14
			return $regs[1];
253
		}
254
255 8
		return false;
256
	}
257
258
	/**
259
	 * Returns URL of the working copy.
260
	 *
261
	 * @param string $wc_path Working copy path.
262
	 *
263
	 * @return string
264
	 * @throws RepositoryCommandException When repository command failed to execute.
265
	 */
266 11
	public function getWorkingCopyUrl($wc_path)
267
	{
268 11
		if ( $this->isUrl($wc_path) ) {
269 2
			return $this->removeCredentials($wc_path);
270
		}
271
272
		try {
273
			// TODO: No exception is thrown, when we have a valid cache, but SVN client was upgraded.
274 9
			$wc_url = (string)$this->_getSvnInfoEntry($wc_path, self::SVN_INFO_CACHE_DURATION)->url;
275
		}
276 9
		catch ( RepositoryCommandException $e ) {
277 3
			if ( $e->getCode() == RepositoryCommandException::SVN_ERR_WC_UPGRADE_REQUIRED ) {
278 2
				$message = explode(PHP_EOL, $e->getMessage());
279
280 2
				$this->_io->writeln(array('', '<error>' . end($message) . '</error>', ''));
281
282 2
				if ( $this->_io->askConfirmation('Run "svn upgrade"', false) ) {
283 1
					$this->getCommand('upgrade', array($wc_path))->runLive();
284
285 1
					return $this->getWorkingCopyUrl($wc_path);
286
				}
287 1
			}
288
289 2
			throw $e;
290
		}
291
292 6
		return $wc_url;
293
	}
294
295
	/**
296
	 * Returns last changed revision on path/url.
297
	 *
298
	 * @param string $path_or_url Path or url.
299
	 *
300
	 * @return integer
301
	 */
302 9
	public function getLastRevision($path_or_url)
303
	{
304
		// Cache "svn info" commands to remote urls, not the working copy.
305 9
		$cache_duration = $this->isUrl($path_or_url) ? $this->_lastRevisionCacheDuration : null;
306
307 9
		return (int)$this->_getSvnInfoEntry($path_or_url, $cache_duration)->commit['revision'];
308
	}
309
310
	/**
311
	 * Determines if given path is in fact an url.
312
	 *
313
	 * @param string $path Path.
314
	 *
315
	 * @return boolean
316
	 */
317 29
	public function isUrl($path)
318
	{
319 29
		return strpos($path, '://') !== false;
320
	}
321
322
	/**
323
	 * Removes credentials from url.
324
	 *
325
	 * @param string $url URL.
326
	 *
327
	 * @return string
328
	 * @throws \InvalidArgumentException When non-url given.
329
	 */
330 17
	public function removeCredentials($url)
331
	{
332 17
		if ( !$this->isUrl($url) ) {
333 1
			throw new \InvalidArgumentException('Unable to remove credentials from "' . $url . '" path.');
334
		}
335
336 16
		return preg_replace('#^(.*)://(.*)@(.*)$#', '$1://$3', $url);
337
	}
338
339
	/**
340
	 * Returns project url (container for "trunk/branches/tags/releases" folders).
341
	 *
342
	 * @param string $repository_url Repository url.
343
	 *
344
	 * @return string
345
	 * @see    getRefByPath
346
	 */
347 9
	public function getProjectUrl($repository_url)
348
	{
349 9
		if ( preg_match('#^(.*?)/(trunk|branches|tags|releases).*$#', $repository_url, $regs) ) {
350 8
			return $regs[1];
351
		}
352
353
		// When known folder structure not detected consider, that project url was already given.
354 1
		return $repository_url;
355
	}
356
357
	/**
358
	 * Returns "svn info" entry for path or url.
359
	 *
360
	 * @param string $path_or_url    Path or url.
361
	 * @param mixed  $cache_duration Cache duration.
362
	 *
363
	 * @return \SimpleXMLElement
364
	 * @throws \LogicException When unexpected 'svn info' results retrieved.
365
	 */
366 24
	private function _getSvnInfoEntry($path_or_url, $cache_duration = null)
367
	{
368
		// Cache "svn info" commands to remote urls, not the working copy.
369 24
		if ( !isset($cache_duration) && $this->isUrl($path_or_url) ) {
370
			$cache_duration = self::SVN_INFO_CACHE_DURATION;
371
		}
372
373
		// Remove credentials from url, because "svn info" fails, when used on repository root.
374 24
		if ( $this->isUrl($path_or_url) ) {
375 12
			$path_or_url = $this->removeCredentials($path_or_url);
376 12
		}
377
378
		// Escape "@" in path names, because peg revision syntax (path@revision) isn't used in here.
379 24
		$path_or_url_escaped = $path_or_url;
380
381 24
		if ( strpos($path_or_url, '@') !== false ) {
382 1
			$path_or_url_escaped .= '@';
383 1
		}
384
385
		// TODO: When wc path (not url) is given, then credentials can be present in "svn info" result anyway.
386 24
		$svn_info = $this
387 24
			->withCache($cache_duration)
388 24
			->getCommand('info', array('--xml', $path_or_url_escaped))
389 24
			->run();
390
391
		// When getting remote "svn info", then path is last folder only.
392 22
		$svn_info_path = (string)$svn_info->entry['path'];
393
394
		// In SVN 1.7+, when doing "svn info" on repository root url.
395 22
		if ( $svn_info_path === '.' ) {
396 1
			$svn_info_path = $path_or_url;
397 1
		}
398
399 22
		if ( basename($svn_info_path) != basename($path_or_url) ) {
400 1
			throw new \LogicException('The directory "' . $path_or_url . '" not found in "svn info" command results.');
401
		}
402
403 21
		return $svn_info->entry;
404
	}
405
406
	/**
407
	 * Returns revision, when path was added to repository.
408
	 *
409
	 * @param string $url Url.
410
	 *
411
	 * @return integer
412
	 * @throws \InvalidArgumentException When not an url was given.
413
	 */
414
	public function getFirstRevision($url)
415
	{
416
		if ( !$this->isUrl($url) ) {
417
			throw new \InvalidArgumentException('The repository URL "' . $url . '" is invalid.');
418
		}
419
420
		$log = $this->withCache('1 year')
421
			->getCommand('log', array('-r', '1:HEAD', '--limit', 1, '--xml', $url))
422
			->run();
423
424
		return (int)$log->logentry['revision'];
425
	}
426
427
	/**
428
	 * Returns conflicts in working copy.
429
	 *
430
	 * @param string $wc_path Working copy path.
431
	 *
432
	 * @return array
433
	 */
434 2
	public function getWorkingCopyConflicts($wc_path)
435
	{
436 2
		$ret = array();
437
438 2
		foreach ( $this->getWorkingCopyStatus($wc_path) as $path => $status ) {
439 2
			if ( $this->isWorkingCopyPathStatus($status, self::STATUS_CONFLICTED) ) {
440 1
				$ret[] = $path;
441 1
			}
442 2
		}
443
444 2
		return $ret;
445
	}
446
447
	/**
448
	 * Returns missing paths in working copy.
449
	 *
450
	 * @param string $wc_path Working copy path.
451
	 *
452
	 * @return array
453
	 */
454 2
	public function getWorkingCopyMissing($wc_path)
455
	{
456 2
		$ret = array();
457
458 2
		foreach ( $this->getWorkingCopyStatus($wc_path) as $path => $status ) {
459 1
			if ( $this->isWorkingCopyPathStatus($status, self::STATUS_MISSING) ) {
460 1
				$ret[] = $path;
461 1
			}
462 2
		}
463
464 2
		return $ret;
465
	}
466
467
	/**
468
	 * Returns compact working copy status.
469
	 *
470
	 * @param string      $wc_path         Working copy path.
471
	 * @param string|null $changelist      Changelist.
472
	 * @param array       $except_statuses Except statuses.
473
	 *
474
	 * @return string
475
	 */
476
	public function getCompactWorkingCopyStatus(
477
		$wc_path,
478
		$changelist = null,
479
		array $except_statuses = array(self::STATUS_UNVERSIONED, self::STATUS_EXTERNAL)
480
	) {
481
		$ret = array();
482
483
		foreach ( $this->getWorkingCopyStatus($wc_path, $changelist, $except_statuses) as $path => $status ) {
484
			$line = $this->getShortItemStatus($status['item']); // Path status.
485
			$line .= $this->getShortPropertiesStatus($status['props']); // Properties status.
486
			$line .= ' '; // Locked status.
487
			$line .= $status['copied'] === true ? '+' : ' '; // Copied status.
488
			$line .= ' ' . $path;
489
490
			$ret[] = $line;
491
		}
492
493
		return implode(PHP_EOL, $ret);
494
	}
495
496
	/**
497
	 * Returns short item status.
498
	 *
499
	 * @param string $status Status.
500
	 *
501
	 * @return string
502
	 * @throws \InvalidArgumentException When unknown status given.
503
	 */
504
	protected function getShortItemStatus($status)
505
	{
506
		$status_map = array(
507
			'added' => 'A',
508
			'conflicted' => 'C',
509
			'deleted' => 'D',
510
			'external' => 'X',
511
			'ignored' => 'I',
512
			// 'incomplete' => '',
513
			// 'merged' => '',
514
			'missing' => '!',
515
			'modified' => 'M',
516
			'none' => ' ',
517
			'normal' => '_',
518
			// 'obstructed' => '',
519
			'replaced' => 'R',
520
			'unversioned' => '?',
521
		);
522
523
		if ( !isset($status_map[$status]) ) {
524
			throw new \InvalidArgumentException('The "' . $status . '" item status is unknown.');
525
		}
526
527
		return $status_map[$status];
528
	}
529
530
	/**
531
	 * Returns short item status.
532
	 *
533
	 * @param string $status Status.
534
	 *
535
	 * @return string
536
	 * @throws \InvalidArgumentException When unknown status given.
537
	 */
538
	protected function getShortPropertiesStatus($status)
539
	{
540
		$status_map = array(
541
			'conflicted' => 'C',
542
			'modified' => 'M',
543
			'normal' => '_',
544
			'none' => ' ',
545
		);
546
547
		if ( !isset($status_map[$status]) ) {
548
			throw new \InvalidArgumentException('The "' . $status . '" properties status is unknown.');
549
		}
550
551
		return $status_map[$status];
552
	}
553
554
	/**
555
	 * Returns working copy status.
556
	 *
557
	 * @param string      $wc_path         Working copy path.
558
	 * @param string|null $changelist      Changelist.
559
	 * @param array       $except_statuses Except statuses.
560
	 *
561
	 * @return array
562
	 * @throws \InvalidArgumentException When changelist doens't exist.
563
	 */
564 10
	public function getWorkingCopyStatus(
565
		$wc_path,
566
		$changelist = null,
567
		array $except_statuses = array(self::STATUS_UNVERSIONED, self::STATUS_EXTERNAL)
568
	) {
569 10
		$all_paths = array();
570
571 10
		$status = $this->getCommand('status', array('--xml', $wc_path))->run();
572
573 10
		if ( empty($changelist) ) {
574
			// Accept all entries from "target" and "changelist" nodes.
575 8
			foreach ( $status->children() as $entries ) {
576 8
				$child_name = $entries->getName();
577
578 8
				if ( $child_name === 'target' || $child_name === 'changelist' ) {
579 7
					$all_paths += $this->processStatusEntryNodes($wc_path, $entries);
0 ignored issues
show
Bug introduced by
It seems like $entries can also be of type null; however, parameter $entries of ConsoleHelpers\SVNBuddy\...ocessStatusEntryNodes() 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

579
					$all_paths += $this->processStatusEntryNodes($wc_path, /** @scrutinizer ignore-type */ $entries);
Loading history...
580 7
				}
581 8
			}
582 8
		}
583
		else {
584
			// Accept all entries from "changelist" node and parent folders from "target" node.
585 2
			foreach ( $status->changelist as $changelist_entries ) {
586 2
				if ( (string)$changelist_entries['name'] === $changelist ) {
587 1
					$all_paths += $this->processStatusEntryNodes($wc_path, $changelist_entries);
588 1
				}
589 2
			}
590
591 2
			if ( !$all_paths ) {
592 1
				throw new \InvalidArgumentException('The "' . $changelist . '" changelist doens\'t exist.');
593
			}
594
595 1
			$parent_paths = $this->getParentPaths(array_keys($all_paths));
596
597 1
			foreach ( $status->target as $target_entries ) {
598 1
				foreach ( $this->processStatusEntryNodes($wc_path, $target_entries) as $path => $path_data ) {
599 1
					if ( in_array($path, $parent_paths) ) {
600 1
						$all_paths[$path] = $path_data;
601 1
					}
602 1
				}
603 1
			}
604
605 1
			ksort($all_paths, SORT_STRING);
606
		}
607
608 9
		$changed_paths = array();
609
610 9
		foreach ( $all_paths as $path => $status ) {
611
			// Exclude paths, that haven't changed (e.g. from changelists).
612 8
			if ( $this->isWorkingCopyPathStatus($status, self::STATUS_NORMAL) ) {
613 6
				continue;
614
			}
615
616
			// Exclude paths with requested statuses.
617 8
			if ( $except_statuses ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $except_statuses 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...
618 7
				foreach ( $except_statuses as $except_status ) {
619 7
					if ( $this->isWorkingCopyPathStatus($status, $except_status) ) {
620 3
						continue 2;
621
					}
622 7
				}
623 7
			}
624
625 8
			$changed_paths[$path] = $status;
626 9
		}
627
628 9
		return $changed_paths;
629
	}
630
631
	/**
632
	 * Processes "entry" nodes from "svn status" command.
633
	 *
634
	 * @param string            $wc_path Working copy path.
635
	 * @param \SimpleXMLElement $entries Entries.
636
	 *
637
	 * @return array
638
	 */
639 8
	protected function processStatusEntryNodes($wc_path, \SimpleXMLElement $entries)
640
	{
641 8
		$ret = array();
642
643 8
		foreach ( $entries as $entry ) {
644 8
			$path = (string)$entry['path'];
645 8
			$path = $path === $wc_path ? '.' : str_replace($wc_path . '/', '', $path);
646
647 8
			$ret[$path] = array(
648 8
				'item' => (string)$entry->{'wc-status'}['item'],
649 8
				'props' => (string)$entry->{'wc-status'}['props'],
650 8
				'tree-conflicted' => (string)$entry->{'wc-status'}['tree-conflicted'] === 'true',
651 8
				'copied' => (string)$entry->{'wc-status'}['copied'] === 'true',
652
			);
653 8
		}
654
655 8
		return $ret;
656
	}
657
658
	/**
659
	 * Detects specific path status.
660
	 *
661
	 * @param array  $status      Path status.
662
	 * @param string $path_status Expected path status.
663
	 *
664
	 * @return boolean
665
	 */
666 8
	protected function isWorkingCopyPathStatus(array $status, $path_status)
667
	{
668 8
		$tree_conflicted = $status['tree-conflicted'];
669
670 8
		if ( $path_status === self::STATUS_NORMAL ) {
671
			// Normal if all of 3 are normal.
672 8
			return $status['item'] === $path_status
673 8
				&& ($status['props'] === $path_status || $status['props'] === self::STATUS_NONE)
674 8
				&& !$tree_conflicted;
675
		}
676 7
		elseif ( $path_status === self::STATUS_CONFLICTED ) {
677
			// Conflict if any of 3 has conflict.
678 2
			return $status['item'] === $path_status || $status['props'] === $path_status || $tree_conflicted;
679
		}
680 7
		elseif ( $path_status === self::STATUS_UNVERSIONED ) {
681 7
			return $status['item'] === $path_status && $status['props'] === self::STATUS_NONE;
682
		}
683
684 7
		return $status['item'] === $path_status;
685
	}
686
687
	/**
688
	 * Returns parent paths from given paths.
689
	 *
690
	 * @param array $paths Paths.
691
	 *
692
	 * @return array
693
	 */
694 1
	protected function getParentPaths(array $paths)
695
	{
696 1
		$ret = array();
697
698 1
		foreach ( $paths as $path ) {
699 1
			while ( $path !== '.' ) {
700 1
				$path = dirname($path);
701 1
				$ret[] = $path;
702 1
			}
703 1
		}
704
705 1
		return array_unique($ret);
706
	}
707
708
	/**
709
	 * Returns working copy changelists.
710
	 *
711
	 * @param string $wc_path Working copy path.
712
	 *
713
	 * @return array
714
	 */
715 2
	public function getWorkingCopyChangelists($wc_path)
716
	{
717 2
		$ret = array();
718 2
		$status = $this->getCommand('status', array('--xml', $wc_path))->run();
719
720 2
		foreach ( $status->changelist as $changelist ) {
721 1
			$ret[] = (string)$changelist['name'];
722 2
		}
723
724 2
		sort($ret, SORT_STRING);
725
726 2
		return $ret;
727
	}
728
729
	/**
730
	 * Returns revisions of paths in a working copy.
731
	 *
732
	 * @param string $wc_path Working copy path.
733
	 *
734
	 * @return array
735
	 */
736
	public function getWorkingCopyRevisions($wc_path)
737
	{
738
		$revisions = array();
739
		$status = $this->getCommand('status', array('--xml', '--verbose', $wc_path))->run();
740
741
		foreach ( $status->target as $target ) {
742
			if ( (string)$target['path'] !== $wc_path ) {
743
				continue;
744
			}
745
746
			foreach ( $target as $entry ) {
747
				$revision = (int)$entry->{'wc-status'}['revision'];
748
				$revisions[$revision] = true;
749
			}
750
		}
751
752
		// The "-1" revision happens, when external is deleted.
753
		// The "0" happens for not committed paths (e.g. added).
754
		unset($revisions[-1], $revisions[0]);
755
756
		return array_keys($revisions);
757
	}
758
759
	/**
760
	 * Determines if there is a working copy on a given path.
761
	 *
762
	 * @param string $path Path.
763
	 *
764
	 * @return boolean
765
	 * @throws \InvalidArgumentException When path isn't found.
766
	 * @throws RepositoryCommandException When repository command failed to execute.
767
	 */
768
	public function isWorkingCopy($path)
769
	{
770
		if ( $this->isUrl($path) || !file_exists($path) ) {
771
			throw new \InvalidArgumentException('Path "' . $path . '" not found.');
772
		}
773
774
		try {
775
			$wc_url = $this->getWorkingCopyUrl($path);
776
		}
777
		catch ( RepositoryCommandException $e ) {
778
			if ( $e->getCode() == RepositoryCommandException::SVN_ERR_WC_NOT_WORKING_COPY ) {
779
				return false;
780
			}
781
782
			throw $e;
783
		}
784
785
		return $wc_url != '';
786
	}
787
788
	/**
789
	 * Returns list of add/removed revisions from last merge operation.
790
	 *
791
	 * @param string  $wc_path            Working copy path, where merge happens.
792
	 * @param boolean $regular_or_reverse Merge direction ("regular" or "reverse").
793
	 *
794
	 * @return array
795
	 */
796 4
	public function getMergedRevisionChanges($wc_path, $regular_or_reverse)
797
	{
798 4
		$final_paths = array();
799
800 4
		if ( $regular_or_reverse ) {
801 2
			$old_paths = $this->getMergedRevisions($wc_path, 'BASE');
0 ignored issues
show
Bug introduced by
'BASE' of type string is incompatible with the type integer expected by parameter $revision of ConsoleHelpers\SVNBuddy\...r::getMergedRevisions(). ( Ignorable by Annotation )

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

801
			$old_paths = $this->getMergedRevisions($wc_path, /** @scrutinizer ignore-type */ 'BASE');
Loading history...
802 2
			$new_paths = $this->getMergedRevisions($wc_path);
803 2
		}
804
		else {
805 2
			$old_paths = $this->getMergedRevisions($wc_path);
806 2
			$new_paths = $this->getMergedRevisions($wc_path, 'BASE');
807
		}
808
809 4
		if ( $old_paths === $new_paths ) {
810 2
			return array();
811
		}
812
813 2
		foreach ( $new_paths as $new_path => $new_merged_revisions ) {
814 2
			if ( !isset($old_paths[$new_path]) ) {
815
				// Merge from new path.
816 2
				$final_paths[$new_path] = $this->_revisionListParser->expandRanges(
817 2
					explode(',', $new_merged_revisions)
818 2
				);
819 2
			}
820 2
			elseif ( $new_merged_revisions != $old_paths[$new_path] ) {
821
				// Merge on existing path.
822 2
				$new_merged_revisions_parsed = $this->_revisionListParser->expandRanges(
823 2
					explode(',', $new_merged_revisions)
824 2
				);
825 2
				$old_merged_revisions_parsed = $this->_revisionListParser->expandRanges(
826 2
					explode(',', $old_paths[$new_path])
827 2
				);
828 2
				$final_paths[$new_path] = array_values(
829 2
					array_diff($new_merged_revisions_parsed, $old_merged_revisions_parsed)
830 2
				);
831 2
			}
832 2
		}
833
834 2
		return $final_paths;
835
	}
836
837
	/**
838
	 * Returns list of merged revisions per path.
839
	 *
840
	 * @param string  $wc_path  Merge target: working copy path.
841
	 * @param integer $revision Revision.
842
	 *
843
	 * @return array
844
	 */
845 4
	protected function getMergedRevisions($wc_path, $revision = null)
846
	{
847 4
		$paths = array();
848
849 4
		$merge_info = $this->getProperty('svn:mergeinfo', $wc_path, $revision);
850 4
		$merge_info = array_filter(explode("\n", $merge_info));
851
852 4
		foreach ( $merge_info as $merge_info_line ) {
853 4
			list($path, $revisions) = explode(':', $merge_info_line, 2);
854 4
			$paths[$path] = $revisions;
855 4
		}
856
857 4
		return $paths;
858
	}
859
860
	/**
861
	 * Returns file contents at given revision.
862
	 *
863
	 * @param string         $path_or_url Path or url.
864
	 * @param integer|string $revision    Revision.
865
	 *
866
	 * @return string
867
	 */
868 1
	public function getFileContent($path_or_url, $revision)
869
	{
870 1
		return $this
871 1
			->withCache(self::SVN_CAT_CACHE_DURATION)
872 1
			->getCommand('cat', array($path_or_url, '--revision', $revision))
873 1
			->run();
874
	}
875
876
}
877