Completed
Push — master ( 79b03c...d0c016 )
by Alexander
02:29
created

Connector::getWorkingCopyChangelists()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 13
ccs 8
cts 8
cp 1
rs 9.4285
cc 2
eloc 7
nc 2
nop 1
crap 2
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
20
/**
21
 * Executes command on the repository.
22
 */
23
class Connector
24
{
25
26
	const STATUS_NORMAL = 'normal';
27
28
	const STATUS_ADDED = 'added';
29
30
	const STATUS_CONFLICTED = 'conflicted';
31
32
	const STATUS_UNVERSIONED = 'unversioned';
33
34
	const STATUS_NONE = 'none';
35
36
	const URL_REGEXP = '#([\w]*)://([^/@\s\']+@)?([^/@:\s\']+)(:\d+)?([^@\s\']*)?#';
37
38
	const SVN_INFO_CACHE_DURATION = '1 year';
39
40
	/**
41
	 * Reference to configuration.
42
	 *
43
	 * @var ConfigEditor
44
	 */
45
	private $_configEditor;
46
47
	/**
48
	 * Process factory.
49
	 *
50
	 * @var IProcessFactory
51
	 */
52
	private $_processFactory;
53
54
	/**
55
	 * Console IO.
56
	 *
57
	 * @var ConsoleIO
58
	 */
59
	private $_io;
60
61
	/**
62
	 * Cache manager.
63
	 *
64
	 * @var CacheManager
65
	 */
66
	private $_cacheManager;
67
68
	/**
69
	 * Path to an svn command.
70
	 *
71
	 * @var string
72
	 */
73
	private $_svnCommand = 'svn';
74
75
	/**
76
	 * Cache duration for next invoked command.
77
	 *
78
	 * @var mixed
79
	 */
80
	private $_nextCommandCacheDuration = 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 IProcessFactory $process_factory Process factory.
94
	 * @param ConsoleIO       $io              Console IO.
95
	 * @param CacheManager    $cache_manager   Cache manager.
96
	 */
97 78
	public function __construct(
98
		ConfigEditor $config_editor,
99
		IProcessFactory $process_factory,
100
		ConsoleIO $io,
101
		CacheManager $cache_manager
102
	) {
103 78
		$this->_configEditor = $config_editor;
104 78
		$this->_processFactory = $process_factory;
105 78
		$this->_io = $io;
106 78
		$this->_cacheManager = $cache_manager;
107
108 78
		$cache_duration = $this->_configEditor->get('repository-connector.last-revision-cache-duration');
109
110 78
		if ( (string)$cache_duration === '' || substr($cache_duration, 0, 1) === '0' ) {
111 4
			$cache_duration = 0;
112 4
		}
113
114 78
		$this->_lastRevisionCacheDuration = $cache_duration;
115
116 78
		$this->prepareSvnCommand();
117 78
	}
118
119
	/**
120
	 * Prepares static part of svn command to be used across the script.
121
	 *
122
	 * @return void
123
	 */
124 78
	protected function prepareSvnCommand()
125
	{
126 78
		$username = $this->_configEditor->get('repository-connector.username');
127 78
		$password = $this->_configEditor->get('repository-connector.password');
128
129 78
		$this->_svnCommand .= ' --non-interactive';
130
131 78
		if ( $username ) {
132 15
			$this->_svnCommand .= ' --username ' . $username;
133 15
		}
134
135 78
		if ( $password ) {
136 15
			$this->_svnCommand .= ' --password ' . $password;
137 15
		}
138 78
	}
139
140
	/**
141
	 * Builds a command.
142
	 *
143
	 * @param string      $sub_command  Sub command.
144
	 * @param string|null $param_string Parameter string.
145
	 *
146
	 * @return Command
147
	 */
148 39
	public function getCommand($sub_command, $param_string = null)
149
	{
150 39
		$command_line = $this->buildCommand($sub_command, $param_string);
151
152 38
		$command = new Command(
153 38
			$command_line,
154 38
			$this->_io,
155 38
			$this->_cacheManager,
156 38
			$this->_processFactory
157 38
		);
158
159 38
		if ( isset($this->_nextCommandCacheDuration) ) {
160 19
			$command->setCacheDuration($this->_nextCommandCacheDuration);
161 19
			$this->_nextCommandCacheDuration = null;
162 19
		}
163
164 38
		return $command;
165
	}
166
167
	/**
168
	 * Builds command from given arguments.
169
	 *
170
	 * @param string $sub_command  Command.
171
	 * @param string $param_string Parameter string.
172
	 *
173
	 * @return string
174
	 * @throws \InvalidArgumentException When command contains spaces.
175
	 */
176 39
	protected function buildCommand($sub_command, $param_string = null)
177
	{
178 39
		if ( strpos($sub_command, ' ') !== false ) {
179 1
			throw new \InvalidArgumentException('The "' . $sub_command . '" sub-command contains spaces.');
180
		}
181
182 38
		$command_line = $this->_svnCommand;
183
184 38
		if ( !empty($sub_command) ) {
185 33
			$command_line .= ' ' . $sub_command;
186 33
		}
187
188 38
		if ( !empty($param_string) ) {
189 36
			$command_line .= ' ' . $param_string;
190 36
		}
191
192 38
		$command_line = preg_replace_callback(
193 38
			'/\{([^\}]*)\}/',
194 38
			function (array $matches) {
195 30
				return escapeshellarg($matches[1]);
196 38
			},
197
			$command_line
198 38
		);
199
200 38
		return $command_line;
201
	}
202
203
	/**
204
	 * Sets cache configuration for next created command.
205
	 *
206
	 * @param mixed $cache_duration Cache duration.
207
	 *
208
	 * @return self
209
	 */
210 20
	public function withCache($cache_duration)
211
	{
212 20
		$this->_nextCommandCacheDuration = $cache_duration;
213
214 20
		return $this;
215
	}
216
217
	/**
218
	 * Returns property value.
219
	 *
220
	 * @param string $name        Property name.
221
	 * @param string $path_or_url Path to get property from.
222
	 * @param mixed  $revision    Revision.
223
	 *
224
	 * @return string
225
	 */
226 2
	public function getProperty($name, $path_or_url, $revision = null)
227
	{
228 2
		$param_string = $name . ' {' . $path_or_url . '}';
229
230 2
		if ( isset($revision) ) {
231 1
			$param_string .= ' --revision ' . $revision;
232 1
		}
233
234 2
		return $this->getCommand('propget', $param_string)->run();
235
	}
236
237
	/**
238
	 * Returns relative path of given path/url to the root of the repository.
239
	 *
240
	 * @param string $path_or_url Path or url.
241
	 *
242
	 * @return string
243
	 */
244 3
	public function getRelativePath($path_or_url)
245
	{
246 3
		$svn_info_entry = $this->_getSvnInfoEntry($path_or_url, self::SVN_INFO_CACHE_DURATION);
247
248 3
		return preg_replace(
249 3
			'/^' . preg_quote($svn_info_entry->repository->root, '/') . '/',
250 3
			'',
251 3
			(string)$svn_info_entry->url,
252
			1
253 3
		);
254
	}
255
256
	/**
257
	 * Returns repository root url from given path/url.
258
	 *
259
	 * @param string $path_or_url Path or url.
260
	 *
261
	 * @return string
262
	 */
263 3
	public function getRootUrl($path_or_url)
264
	{
265 3
		return (string)$this->_getSvnInfoEntry($path_or_url, self::SVN_INFO_CACHE_DURATION)->repository->root;
266
	}
267
268
	/**
269
	 * Determines if path is a root of the ref.
270
	 *
271
	 * @param string $path Path to a file.
272
	 *
273
	 * @return boolean
274
	 */
275 13
	public function isRefRoot($path)
276
	{
277 13
		$ref = $this->getRefByPath($path);
278
279 13
		if ( $ref === false ) {
280 4
			return false;
281
		}
282
283 9
		return preg_match('#/' . preg_quote($ref, '#') . '/$#', $path) > 0;
284
	}
285
286
	/**
287
	 * Detects ref from given path.
288
	 *
289
	 * @param string $path Path to a file.
290
	 *
291
	 * @return string|boolean
292
	 * @see    getProjectUrl
293
	 */
294 22
	public function getRefByPath($path)
295
	{
296 22
		if ( preg_match('#^.*?/(trunk|branches/[^/]+|tags/[^/]+|releases/[^/]+).*$#', $path, $regs) ) {
297 14
			return $regs[1];
298
		}
299
300 8
		return false;
301
	}
302
303
	/**
304
	 * Returns URL of the working copy.
305
	 *
306
	 * @param string $wc_path Working copy path.
307
	 *
308
	 * @return string
309
	 * @throws RepositoryCommandException When repository command failed to execute.
310
	 */
311 8
	public function getWorkingCopyUrl($wc_path)
312
	{
313 8
		if ( $this->isUrl($wc_path) ) {
314 2
			return $this->removeCredentials($wc_path);
315
		}
316
317
		try {
318 6
			$wc_url = (string)$this->_getSvnInfoEntry($wc_path, self::SVN_INFO_CACHE_DURATION)->url;
319
		}
320 6
		catch ( RepositoryCommandException $e ) {
321 3
			if ( $e->getCode() == RepositoryCommandException::SVN_ERR_WC_UPGRADE_REQUIRED ) {
322 2
				$message = explode(PHP_EOL, $e->getMessage());
323
324 2
				$this->_io->writeln(array('', '<error>' . end($message) . '</error>', ''));
325
326 2
				if ( $this->_io->askConfirmation('Run "svn upgrade"', false) ) {
327 1
					$this->getCommand('upgrade', '{' . $wc_path . '}')->runLive();
328
329 1
					return $this->getWorkingCopyUrl($wc_path);
330
				}
331 1
			}
332
333 2
			throw $e;
334
		}
335
336 3
		return $wc_url;
337
	}
338
339
	/**
340
	 * Returns last changed revision on path/url.
341
	 *
342
	 * @param string $path_or_url Path or url.
343
	 *
344
	 * @return integer
345
	 */
346 7
	public function getLastRevision($path_or_url)
347
	{
348
		// Cache "svn info" commands to remote urls, not the working copy.
349 7
		$cache_duration = $this->isUrl($path_or_url) ? $this->_lastRevisionCacheDuration : null;
350
351 7
		return (int)$this->_getSvnInfoEntry($path_or_url, $cache_duration)->commit['revision'];
352
	}
353
354
	/**
355
	 * Determines if given path is in fact an url.
356
	 *
357
	 * @param string $path Path.
358
	 *
359
	 * @return boolean
360
	 */
361 24
	public function isUrl($path)
362
	{
363 24
		return strpos($path, '://') !== false;
364
	}
365
366
	/**
367
	 * Removes credentials from url.
368
	 *
369
	 * @param string $url URL.
370
	 *
371
	 * @return string
372
	 * @throws \InvalidArgumentException When non-url given.
373
	 */
374 15
	public function removeCredentials($url)
375
	{
376 15
		if ( !$this->isUrl($url) ) {
377 1
			throw new \InvalidArgumentException('Unable to remove credentials from "' . $url . '" path.');
378
		}
379
380 14
		return preg_replace('#^(.*)://(.*)@(.*)$#', '$1://$3', $url);
381
	}
382
383
	/**
384
	 * Returns project url (container for "trunk/branches/tags/releases" folders).
385
	 *
386
	 * @param string $repository_url Repository url.
387
	 *
388
	 * @return string
389
	 * @see    getRefByPath
390
	 */
391 9
	public function getProjectUrl($repository_url)
392
	{
393 9
		if ( preg_match('#^(.*?)/(trunk|branches|tags|releases).*$#', $repository_url, $regs) ) {
394 8
			return $regs[1];
395
		}
396
397
		// When known folder structure not detected consider, that project url was already given.
398 1
		return $repository_url;
399
	}
400
401
	/**
402
	 * Returns "svn info" entry for path or url.
403
	 *
404
	 * @param string $path_or_url    Path or url.
405
	 * @param mixed  $cache_duration Cache duration.
406
	 *
407
	 * @return \SimpleXMLElement
408
	 * @throws \LogicException When unexpected 'svn info' results retrieved.
409
	 */
410 19
	private function _getSvnInfoEntry($path_or_url, $cache_duration = null)
411
	{
412
		// Cache "svn info" commands to remote urls, not the working copy.
413 19
		if ( !isset($cache_duration) && $this->isUrl($path_or_url) ) {
414
			$cache_duration = self::SVN_INFO_CACHE_DURATION;
415
		}
416
417
		// Remove credentials from url, because "svn info" fails, when used on repository root.
418 19
		if ( $this->isUrl($path_or_url) ) {
419 10
			$path_or_url = $this->removeCredentials($path_or_url);
420 10
		}
421
422
		// TODO: When wc path (not url) is given, then credentials can be present in "svn info" result anyway.
423 19
		$svn_info = $this->withCache($cache_duration)->getCommand('info', '--xml {' . $path_or_url . '}')->run();
424
425
		// When getting remote "svn info", then path is last folder only.
426 17
		$svn_info_path = $this->_getSvnInfoEntryPath($svn_info->entry);
427
428
		// In SVN 1.7+, when doing "svn info" on repository root url.
429 17
		if ( $svn_info_path === '.' ) {
430
			$svn_info_path = $path_or_url;
431
		}
432
433 17
		if ( basename($svn_info_path) != basename($path_or_url) ) {
434 1
			throw new \LogicException('The directory "' . $path_or_url . '" not found in "svn info" command results.');
435
		}
436
437 16
		return $svn_info->entry;
438
	}
439
440
	/**
441
	 * Returns path of "svn info" entry.
442
	 *
443
	 * @param \SimpleXMLElement $svn_info_entry The "entry" node of "svn info" command.
444
	 *
445
	 * @return string
446
	 */
447 17
	private function _getSvnInfoEntryPath(\SimpleXMLElement $svn_info_entry)
448
	{
449
		// SVN 1.7+.
450 17
		$path = (string)$svn_info_entry->{'wc-info'}->{'wcroot-abspath'};
451
452 17
		if ( $path ) {
453 1
			return $path;
454
		}
455
456
		// SVN 1.6-.
457 16
		return (string)$svn_info_entry['path'];
458
	}
459
460
	/**
461
	 * Returns revision, when path was added to repository.
462
	 *
463
	 * @param string $url Url.
464
	 *
465
	 * @return integer
466
	 * @throws \InvalidArgumentException When not an url was given.
467
	 */
468
	public function getFirstRevision($url)
469
	{
470
		if ( !$this->isUrl($url) ) {
471
			throw new \InvalidArgumentException('The repository URL "' . $url . '" is invalid.');
472
		}
473
474
		$log = $this->withCache('1 year')->getCommand('log', ' -r 1:HEAD --limit 1 --xml {' . $url . '}')->run();
475
476
		return (int)$log->logentry['revision'];
477
	}
478
479
	/**
480
	 * Returns conflicts in working copy.
481
	 *
482
	 * @param string $wc_path Working copy path.
483
	 *
484
	 * @return array
485
	 */
486 2
	public function getWorkingCopyConflicts($wc_path)
487
	{
488 2
		$ret = array();
489
490 2
		foreach ( $this->getWorkingCopyStatus($wc_path) as $path => $status ) {
491 2
			if ( $this->isWorkingCopyPathStatus($status, self::STATUS_CONFLICTED) ) {
492 1
				$ret[] = $path;
493 1
			}
494 2
		}
495
496 2
		return $ret;
497
	}
498
499
	/**
500
	 * Returns compact working copy status.
501
	 *
502
	 * @param string      $wc_path          Working copy path.
503
	 * @param string|null $changelist       Changelist.
504
	 * @param boolean     $with_unversioned With unversioned.
505
	 *
506
	 * @return string
507
	 */
508
	public function getCompactWorkingCopyStatus($wc_path, $changelist = null, $with_unversioned = false)
509
	{
510
		$ret = array();
511
512
		foreach ( $this->getWorkingCopyStatus($wc_path, $changelist, $with_unversioned) as $path => $status ) {
513
			$line = $this->getShortItemStatus($status['item']) . $this->getShortPropertiesStatus($status['props']);
514
			$line .= '   ' . $path;
515
516
			$ret[] = $line;
517
		}
518
519
		return implode(PHP_EOL, $ret);
520
	}
521
522
	/**
523
	 * Returns short item status.
524
	 *
525
	 * @param string $status Status.
526
	 *
527
	 * @return string
528
	 * @throws \InvalidArgumentException When unknown status given.
529
	 */
530
	protected function getShortItemStatus($status)
531
	{
532
		$status_map = array(
533
			'added' => 'A',
534
			'conflicted' => 'C',
535
			'deleted' => 'D',
536
			'external' => 'X',
537
			'ignored' => 'I',
538
			// 'incomplete' => '',
539
			// 'merged' => '',
540
			'missing' => '!',
541
			'modified' => 'M',
542
			'none' => ' ',
543
			'normal' => '_',
544
			// 'obstructed' => '',
545
			'replaced' => 'R',
546
			'unversioned' => '?',
547
		);
548
549
		if ( !isset($status_map[$status]) ) {
550
			throw new \InvalidArgumentException('The "' . $status . '" item status is unknown.');
551
		}
552
553
		return $status_map[$status];
554
	}
555
556
	/**
557
	 * Returns short item status.
558
	 *
559
	 * @param string $status Status.
560
	 *
561
	 * @return string
562
	 * @throws \InvalidArgumentException When unknown status given.
563
	 */
564
	protected function getShortPropertiesStatus($status)
565
	{
566
		$status_map = array(
567
			'conflicted' => 'C',
568
			'modified' => 'M',
569
			'normal' => '_',
570
			'none' => ' ',
571
		);
572
573
		if ( !isset($status_map[$status]) ) {
574
			throw new \InvalidArgumentException('The "' . $status . '" properties status is unknown.');
575
		}
576
577
		return $status_map[$status];
578
	}
579
580
	/**
581
	 * Returns working copy status.
582
	 *
583
	 * @param string      $wc_path          Working copy path.
584
	 * @param string|null $changelist       Changelist.
585
	 * @param boolean     $with_unversioned With unversioned.
586
	 *
587
	 * @return array
588
	 * @throws \InvalidArgumentException When changelist doens't exist.
589
	 */
590 5
	public function getWorkingCopyStatus($wc_path, $changelist = null, $with_unversioned = false)
591
	{
592 5
		$all_paths = array();
593
594 5
		$status = $this->getCommand('status', '--xml {' . $wc_path . '}')->run();
595
596 5
		if ( !strlen($changelist) ) {
597
			// Accept all entries from "target" and "changelist" nodes.
598 3
			foreach ( $status->children() as $entries ) {
599 3
				$child_name = $entries->getName();
600
601 3
				if ( $child_name === 'target' || $child_name === 'changelist' ) {
602 3
					$all_paths += $this->processStatusEntryNodes($wc_path, $entries);
603 3
				}
604 3
			}
605 3
		}
606
		else {
607
			// Accept all entries from "changelist" node and parent folders from "target" node.
608 2
			foreach ( $status->changelist as $changelist_entries ) {
609 2
				if ( (string)$changelist_entries['name'] === $changelist ) {
610 1
					$all_paths += $this->processStatusEntryNodes($wc_path, $changelist_entries);
611 1
				}
612 2
			}
613
614 2
			if ( !$all_paths ) {
615 1
				throw new \InvalidArgumentException('The "' . $changelist . '" changelist doens\'t exist.');
616
			}
617
618 1
			$parent_paths = $this->getParentPaths(array_keys($all_paths));
619
620 1
			foreach ( $status->target as $target_entries ) {
621 1
				foreach ( $this->processStatusEntryNodes($wc_path, $target_entries) as $path => $path_data ) {
622 1
					if ( in_array($path, $parent_paths) ) {
623 1
						$all_paths[$path] = $path_data;
624 1
					}
625 1
				}
626 1
			}
627
628 1
			ksort($all_paths, SORT_STRING);
629
		}
630
631
		// Exclude paths, that haven't changed (e.g. from changelists).
632 4
		$changed_paths = array();
633
634 4
		foreach ( $all_paths as $path => $status ) {
635 4
			if ( $this->isWorkingCopyPathStatus($status, self::STATUS_NORMAL) ) {
636 3
				continue;
637
			}
638
639 4
			if ( !$with_unversioned && $this->isWorkingCopyPathStatus($status, self::STATUS_UNVERSIONED) ) {
640 2
				continue;
641
			}
642
643 4
			$changed_paths[$path] = $status;
644 4
		}
645
646 4
		return $changed_paths;
647
	}
648
649
	/**
650
	 * Processes "entry" nodes from "svn status" command.
651
	 *
652
	 * @param string            $wc_path Working copy path.
653
	 * @param \SimpleXMLElement $entries Entries.
654
	 *
655
	 * @return array
656
	 */
657 4
	protected function processStatusEntryNodes($wc_path, \SimpleXMLElement $entries)
658
	{
659 4
		$ret = array();
660
661 4
		foreach ( $entries as $entry ) {
662 4
			$path = (string)$entry['path'];
663 4
			$path = $path === $wc_path ? '.' : str_replace($wc_path . '/', '', $path);
664
665 4
			$ret[$path] = array(
666 4
				'item' => (string)$entry->{'wc-status'}['item'],
667 4
				'props' => (string)$entry->{'wc-status'}['props'],
668 4
				'tree-conflicted' => (string)$entry->{'wc-status'}['tree-conflicted'] === 'true',
669
			);
670 4
		}
671
672 4
		return $ret;
673
	}
674
675
	/**
676
	 * Detects specific path status.
677
	 *
678
	 * @param array  $status      Path status.
679
	 * @param string $path_status Expected path status.
680
	 *
681
	 * @return boolean
682
	 */
683 4
	protected function isWorkingCopyPathStatus(array $status, $path_status)
684
	{
685 4
		$tree_conflicted = $status['tree-conflicted'];
686
687 4
		if ( $path_status === self::STATUS_NORMAL ) {
688
			// Normal if all of 3 are normal.
689 4
			return $status['item'] === $path_status && $status['props'] === $path_status && !$tree_conflicted;
690
		}
691 4
		elseif ( $path_status === self::STATUS_CONFLICTED ) {
692
			// Conflict if any of 3 has conflict.
693 2
			return $status['item'] === $path_status || $status['props'] === $path_status || $tree_conflicted;
694
		}
695 4
		elseif ( $path_status === self::STATUS_UNVERSIONED ) {
696 4
			return $status['item'] === $path_status && $status['props'] === self::STATUS_NONE;
697
		}
698
699
		return $status['item'] == $path_status;
700
	}
701
702
	/**
703
	 * Returns parent paths from given paths.
704
	 *
705
	 * @param array $paths Paths.
706
	 *
707
	 * @return array
708
	 */
709 1
	protected function getParentPaths(array $paths)
710
	{
711 1
		$ret = array();
712
713 1
		foreach ( $paths as $path ) {
714 1
			while ( $path !== '.' ) {
715 1
				$path = dirname($path);
716 1
				$ret[] = $path;
717 1
			}
718 1
		}
719
720 1
		return array_unique($ret);
721
	}
722
723
	/**
724
	 * Returns working copy changelists.
725
	 *
726
	 * @param string $wc_path Working copy path.
727
	 *
728
	 * @return array
729
	 */
730 2
	public function getWorkingCopyChangelists($wc_path)
731
	{
732 2
		$ret = array();
733 2
		$status = $this->getCommand('status', '--xml {' . $wc_path . '}')->run();
734
735 2
		foreach ( $status->changelist as $changelist ) {
736 1
			$ret[] = (string)$changelist['name'];
737 2
		}
738
739 2
		sort($ret, SORT_STRING);
740
741 2
		return $ret;
742
	}
743
744
	/**
745
	 * Determines if working copy contains mixed revisions.
746
	 *
747
	 * @param string $wc_path Working copy path.
748
	 *
749
	 * @return array
750
	 */
751
	public function isMixedRevisionWorkingCopy($wc_path)
752
	{
753
		$revisions = array();
754
		$status = $this->getCommand('status', '--xml --verbose {' . $wc_path . '}')->run();
755
756
		foreach ( $status->target as $target ) {
757
			if ( (string)$target['path'] !== $wc_path ) {
758
				continue;
759
			}
760
761
			foreach ( $target as $entry ) {
762
				$item_status = (string)$entry->{'wc-status'}['item'];
763
764
				if ( $item_status !== self::STATUS_UNVERSIONED ) {
765
					$revision = (int)$entry->{'wc-status'}['revision'];
766
					$revisions[$revision] = true;
767
				}
768
			}
769
		}
770
771
		return count($revisions) > 1;
772
	}
773
774
	/**
775
	 * Determines if there is a working copy on a given path.
776
	 *
777
	 * @param string $path Path.
778
	 *
779
	 * @return boolean
780
	 * @throws \InvalidArgumentException When path isn't found.
781
	 * @throws RepositoryCommandException When repository command failed to execute.
782
	 */
783
	public function isWorkingCopy($path)
784
	{
785
		if ( $this->isUrl($path) || !file_exists($path) ) {
786
			throw new \InvalidArgumentException('Path "' . $path . '" not found.');
787
		}
788
789
		try {
790
			$wc_url = $this->getWorkingCopyUrl($path);
791
		}
792
		catch ( RepositoryCommandException $e ) {
793
			if ( $e->getCode() == RepositoryCommandException::SVN_ERR_WC_NOT_WORKING_COPY ) {
794
				return false;
795
			}
796
797
			throw $e;
798
		}
799
800
		return $wc_url != '';
801
	}
802
803
}
804