Passed
Push — master ( 96a0c3...3ec7ec )
by Alexander
02:19
created

Connector::getCompactWorkingCopyStatus()   B

Complexity

Conditions 10
Paths 82

Size

Total Lines 44
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 23
dl 0
loc 44
ccs 0
cts 23
cp 0
rs 7.6666
c 2
b 0
f 0
cc 10
nc 82
nop 3
crap 110

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Config\ConfigEditor;
15
use ConsoleHelpers\ConsoleKit\ConsoleIO;
16
use ConsoleHelpers\SVNBuddy\Exception\RepositoryCommandException;
17
use ConsoleHelpers\SVNBuddy\Repository\Parser\RevisionListParser;
18
19
/**
20
 * Executes command on the repository.
21
 */
22
class Connector
23
{
24
25
	const STATUS_NORMAL = 'normal';
26
27
	const STATUS_ADDED = 'added';
28
29
	const STATUS_CONFLICTED = 'conflicted';
30
31
	const STATUS_UNVERSIONED = 'unversioned';
32
33
	const STATUS_EXTERNAL = 'external';
34
35
	const STATUS_MISSING = 'missing';
36
37
	const STATUS_NONE = 'none';
38
39
	const URL_REGEXP = '#([\w]*)://([^/@\s\']+@)?([^/@:\s\']+)(:\d+)?([^@\s\']*)?#';
40
41
	const SVN_INFO_CACHE_DURATION = '1 year';
42
43
	const SVN_CAT_CACHE_DURATION = '1 month';
44
45
	/**
46
	 * Command factory.
47
	 *
48
	 * @var CommandFactory
49
	 */
50
	private $_commandFactory;
51
52
	/**
53
	 * Console IO.
54
	 *
55
	 * @var ConsoleIO
56
	 */
57
	private $_io;
58
59
	/**
60
	 * Revision list parser.
61
	 *
62
	 * @var RevisionListParser
63
	 */
64
	private $_revisionListParser;
65
66
	/**
67
	 * Cache duration for next invoked command.
68
	 *
69
	 * @var mixed
70
	 */
71
	private $_nextCommandCacheDuration = null;
72
73
	/**
74
	 * Cache overwrite for next invoked command.
75
	 *
76
	 * @var mixed
77
	 */
78
	private $_nextCommandCacheOverwrite = null;
79
80
	/**
81
	 * Whatever to cache last repository revision or not.
82
	 *
83
	 * @var mixed
84
	 */
85
	private $_lastRevisionCacheDuration = null;
86
87
	/**
88
	 * Creates repository connector.
89
	 *
90
	 * @param ConfigEditor       $config_editor        ConfigEditor.
91
	 * @param CommandFactory     $command_factory      Command factory.
92
	 * @param ConsoleIO          $io                   Console IO.
93
	 * @param RevisionListParser $revision_list_parser Revision list parser.
94
	 */
95 95
	public function __construct(
96
		ConfigEditor $config_editor,
97
		CommandFactory $command_factory,
98
		ConsoleIO $io,
99
		RevisionListParser $revision_list_parser
100
	) {
101 95
		$this->_commandFactory = $command_factory;
102 95
		$this->_io = $io;
103 95
		$this->_revisionListParser = $revision_list_parser;
104
105 95
		$cache_duration = $config_editor->get('repository-connector.last-revision-cache-duration');
106
107 95
		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

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

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

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

837
			$old_paths = $this->getMergedRevisions($wc_path, /** @scrutinizer ignore-type */ 'BASE');
Loading history...
838 2
			$new_paths = $this->getMergedRevisions($wc_path);
839
		}
840
		else {
841 2
			$old_paths = $this->getMergedRevisions($wc_path);
842 2
			$new_paths = $this->getMergedRevisions($wc_path, 'BASE');
843
		}
844
845 4
		if ( $old_paths === $new_paths ) {
846 2
			return array();
847
		}
848
849 2
		foreach ( $new_paths as $new_path => $new_merged_revisions ) {
850 2
			if ( !isset($old_paths[$new_path]) ) {
851
				// Merge from new path.
852 2
				$final_paths[$new_path] = $this->_revisionListParser->expandRanges(
853 2
					explode(',', $new_merged_revisions)
854 2
				);
855
			}
856 2
			elseif ( $new_merged_revisions != $old_paths[$new_path] ) {
857
				// Merge on existing path.
858 2
				$new_merged_revisions_parsed = $this->_revisionListParser->expandRanges(
859 2
					explode(',', $new_merged_revisions)
860 2
				);
861 2
				$old_merged_revisions_parsed = $this->_revisionListParser->expandRanges(
862 2
					explode(',', $old_paths[$new_path])
863 2
				);
864 2
				$final_paths[$new_path] = array_values(
865 2
					array_diff($new_merged_revisions_parsed, $old_merged_revisions_parsed)
866 2
				);
867
			}
868
		}
869
870 2
		return $final_paths;
871
	}
872
873
	/**
874
	 * Returns list of merged revisions per path.
875
	 *
876
	 * @param string  $wc_path  Merge target: working copy path.
877
	 * @param integer $revision Revision.
878
	 *
879
	 * @return array
880
	 */
881 4
	protected function getMergedRevisions($wc_path, $revision = null)
882
	{
883 4
		$paths = array();
884
885 4
		$merge_info = $this->getProperty('svn:mergeinfo', $wc_path, $revision);
886 4
		$merge_info = array_filter(explode("\n", $merge_info));
887
888 4
		foreach ( $merge_info as $merge_info_line ) {
889 4
			list($path, $revisions) = explode(':', $merge_info_line, 2);
890 4
			$paths[$path] = $revisions;
891
		}
892
893 4
		return $paths;
894
	}
895
896
	/**
897
	 * Returns file contents at given revision.
898
	 *
899
	 * @param string         $path_or_url Path or url.
900
	 * @param integer|string $revision    Revision.
901
	 *
902
	 * @return string
903
	 */
904 1
	public function getFileContent($path_or_url, $revision)
905
	{
906 1
		return $this
907 1
			->withCacheDuration(self::SVN_CAT_CACHE_DURATION)
908 1
			->getCommand('cat', array($path_or_url, '--revision', $revision))
909 1
			->run();
910
	}
911
912
}
913