Passed
Push — master ( 18dfb5...5f6586 )
by Alexander
08:11 queued 06:11
created

Connector::getWorkingCopyMissing()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 11
ccs 8
cts 8
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3
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 97
	public function __construct(
96
		ConfigEditor $config_editor,
97
		CommandFactory $command_factory,
98
		ConsoleIO $io,
99
		RevisionListParser $revision_list_parser
100
	) {
101 97
		$this->_commandFactory = $command_factory;
102 97
		$this->_io = $io;
103 97
		$this->_revisionListParser = $revision_list_parser;
104
105 97
		$cache_duration = $config_editor->get('repository-connector.last-revision-cache-duration');
106
107 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

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

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

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

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