Completed
Push — master ( 45b20d...7ea826 )
by Alexander
03:11
created

Connector   C

Complexity

Total Complexity 71

Size/Duplication

Total Lines 650
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 56.42%

Importance

Changes 15
Bugs 1 Features 5
Metric Value
wmc 71
c 15
b 1
f 5
lcom 1
cbo 4
dl 0
loc 650
ccs 123
cts 218
cp 0.5642
rs 5.3137

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 21 3
A prepareSvnCommand() 0 15 3
A getCommand() 0 18 2
B buildCommand() 0 26 4
A withCache() 0 6 1
A getProperty() 0 10 2
A getRelativePath() 0 11 1
A getRootUrl() 0 4 1
A isRefRoot() 0 10 2
A getRefByPath() 0 8 2
B getWorkingCopyUrl() 0 27 5
A getLastRevision() 0 7 2
A isUrl() 0 4 1
A removeCredentials() 0 8 2
A getProjectUrl() 0 9 2
B _getSvnInfoEntry() 0 29 6
A _getSvnInfoEntryPath() 0 12 2
A getFirstRevision() 0 10 2
B getWorkingCopyConflicts() 0 12 5
A getCompactWorkingCopyStatus() 0 17 4
B getShortItemStatus() 0 25 2
A getShortPropertiesStatus() 0 15 2
B getWorkingCopyStatus() 0 30 5
B isMixedRevisionWorkingCopy() 0 22 5
B isWorkingCopy() 0 19 5

How to fix   Complexity   

Complex Class

Complex classes like Connector often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Connector, and based on these observations, apply Extract Interface, too.

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