Passed
Push — master ( 770863...504002 )
by Alexander
02:22
created

Connector::withCacheDuration()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
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
		$ret = array();
492
493
		foreach ( $this->getWorkingCopyStatus($wc_path, $changelist, $except_statuses) as $path => $status ) {
494
			$line = $this->getShortItemStatus($status['item']); // Path status.
495
			$line .= $this->getShortPropertiesStatus($status['props']); // Properties status.
496
			$line .= ' '; // Locked status.
497
			$line .= $status['copied'] === true ? '+' : ' '; // Copied status.
498
			$line .= ' ' . $path;
499
500
			$ret[] = $line;
501
		}
502
503
		return implode(PHP_EOL, $ret);
504
	}
505
506
	/**
507
	 * Returns short item status.
508
	 *
509
	 * @param string $status Status.
510
	 *
511
	 * @return string
512
	 * @throws \InvalidArgumentException When unknown status given.
513
	 */
514
	protected function getShortItemStatus($status)
515
	{
516
		$status_map = array(
517
			'added' => 'A',
518
			'conflicted' => 'C',
519
			'deleted' => 'D',
520
			'external' => 'X',
521
			'ignored' => 'I',
522
			// 'incomplete' => '',
523
			// 'merged' => '',
524
			'missing' => '!',
525
			'modified' => 'M',
526
			'none' => ' ',
527
			'normal' => '_',
528
			// 'obstructed' => '',
529
			'replaced' => 'R',
530
			'unversioned' => '?',
531
		);
532
533
		if ( !isset($status_map[$status]) ) {
534
			throw new \InvalidArgumentException('The "' . $status . '" item status is unknown.');
535
		}
536
537
		return $status_map[$status];
538
	}
539
540
	/**
541
	 * Returns short item status.
542
	 *
543
	 * @param string $status Status.
544
	 *
545
	 * @return string
546
	 * @throws \InvalidArgumentException When unknown status given.
547
	 */
548
	protected function getShortPropertiesStatus($status)
549
	{
550
		$status_map = array(
551
			'conflicted' => 'C',
552
			'modified' => 'M',
553
			'normal' => '_',
554
			'none' => ' ',
555
		);
556
557
		if ( !isset($status_map[$status]) ) {
558
			throw new \InvalidArgumentException('The "' . $status . '" properties status is unknown.');
559
		}
560
561
		return $status_map[$status];
562
	}
563
564
	/**
565
	 * Returns working copy status.
566
	 *
567
	 * @param string      $wc_path         Working copy path.
568
	 * @param string|null $changelist      Changelist.
569
	 * @param array       $except_statuses Except statuses.
570
	 *
571
	 * @return array
572
	 * @throws \InvalidArgumentException When changelist doens't exist.
573
	 */
574 10
	public function getWorkingCopyStatus(
575
		$wc_path,
576
		$changelist = null,
577
		array $except_statuses = array(self::STATUS_UNVERSIONED, self::STATUS_EXTERNAL)
578
	) {
579 10
		$all_paths = array();
580
581 10
		$status = $this->getCommand('status', array('--xml', $wc_path))->run();
582
583 10
		if ( empty($changelist) ) {
584
			// Accept all entries from "target" and "changelist" nodes.
585 8
			foreach ( $status->children() as $entries ) {
586 8
				$child_name = $entries->getName();
587
588 8
				if ( $child_name === 'target' || $child_name === 'changelist' ) {
589 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

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

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