Passed
Push — master ( a1e767...742af1 )
by Alexander
17:40 queued 15:37
created

Connector::_getSvnInfoEntry()   B

Complexity

Conditions 7
Paths 32

Size

Total Lines 38
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7.0099

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 17
c 2
b 0
f 0
dl 0
loc 38
ccs 16
cts 17
cp 0.9412
rs 8.8333
cc 7
nc 32
nop 2
crap 7.0099
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
		}
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 string|null $param_string Parameter string.
121
	 *
122
	 * @return Command
123
	 */
124 51
	public function getCommand($sub_command, $param_string = null)
125
	{
126 51
		$command = $this->_commandFactory->getCommand($sub_command, $param_string);
127
128 51
		if ( isset($this->_nextCommandCacheDuration) ) {
129 27
			$command->setCacheDuration($this->_nextCommandCacheDuration);
130 27
			$this->_nextCommandCacheDuration = null;
131
		}
132
133 51
		if ( isset($this->_nextCommandCacheOverwrite) ) {
134 4
			$command->setCacheOverwrite($this->_nextCommandCacheOverwrite);
135 4
			$this->_nextCommandCacheOverwrite = null;
136
		}
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
		$param_string = $name . ' {' . $path_or_url . '}';
170
171 8
		if ( isset($revision) ) {
172 7
			$param_string .= ' --revision ' . $revision;
173
		}
174
175
		// The "null" for non-existing properties is never returned, because output is converted to string.
176 8
		$property_value = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $property_value is dead and can be removed.
Loading history...
177
178
		try {
179 8
			$property_value = $this->getCommand('propget', $param_string)->run();
180
		}
181 1
		catch ( RepositoryCommandException $e ) {
182
			// Preserve SVN 1.8- behavior, where reading value of non-existing property returned an empty string.
183 1
			if ( $e->getCode() !== RepositoryCommandException::SVN_ERR_BASE ) {
184
				throw $e;
185
			}
186
		}
187
188 8
		return $property_value;
189
	}
190
191
	/**
192
	 * Returns relative path of given path/url to the root of the repository.
193
	 *
194
	 * @param string $path_or_url Path or url.
195
	 *
196
	 * @return string
197
	 */
198 3
	public function getRelativePath($path_or_url)
199
	{
200 3
		$svn_info_entry = $this->_getSvnInfoEntry($path_or_url, self::SVN_INFO_CACHE_DURATION);
201
202 3
		return preg_replace(
203 3
			'/^' . preg_quote($svn_info_entry->repository->root, '/') . '/',
204 3
			'',
205 3
			(string)$svn_info_entry->url,
206 3
			1
207
		);
208
	}
209
210
	/**
211
	 * Returns repository root url from given path/url.
212
	 *
213
	 * @param string $path_or_url Path or url.
214
	 *
215
	 * @return string
216
	 */
217 3
	public function getRootUrl($path_or_url)
218
	{
219 3
		return (string)$this->_getSvnInfoEntry($path_or_url, self::SVN_INFO_CACHE_DURATION)->repository->root;
220
	}
221
222
	/**
223
	 * Determines if path is a root of the ref.
224
	 *
225
	 * @param string $path Path to a file.
226
	 *
227
	 * @return boolean
228
	 */
229 13
	public function isRefRoot($path)
230
	{
231 13
		$ref = $this->getRefByPath($path);
232
233 13
		if ( $ref === false ) {
234 4
			return false;
235
		}
236
237 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

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

570
		if ( !strlen(/** @scrutinizer ignore-type */ $changelist) ) {
Loading history...
571
			// Accept all entries from "target" and "changelist" nodes.
572 8
			foreach ( $status->children() as $entries ) {
573 8
				$child_name = $entries->getName();
574
575 8
				if ( $child_name === 'target' || $child_name === 'changelist' ) {
576 8
					$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

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

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