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
|
|
|
use ConsoleHelpers\SVNBuddy\Repository\Parser\RevisionListParser; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Executes command on the repository. |
23
|
|
|
*/ |
24
|
|
|
class Connector |
25
|
|
|
{ |
26
|
|
|
|
27
|
|
|
const STATUS_NORMAL = 'normal'; |
28
|
|
|
|
29
|
|
|
const STATUS_ADDED = 'added'; |
30
|
|
|
|
31
|
|
|
const STATUS_CONFLICTED = 'conflicted'; |
32
|
|
|
|
33
|
|
|
const STATUS_UNVERSIONED = 'unversioned'; |
34
|
|
|
|
35
|
|
|
const STATUS_EXTERNAL = 'external'; |
36
|
|
|
|
37
|
|
|
const STATUS_MISSING = 'missing'; |
38
|
|
|
|
39
|
|
|
const STATUS_NONE = 'none'; |
40
|
|
|
|
41
|
|
|
const URL_REGEXP = '#([\w]*)://([^/@\s\']+@)?([^/@:\s\']+)(:\d+)?([^@\s\']*)?#'; |
42
|
|
|
|
43
|
|
|
const SVN_INFO_CACHE_DURATION = '1 year'; |
44
|
|
|
|
45
|
|
|
const SVN_CAT_CACHE_DURATION = '1 month'; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Reference to configuration. |
49
|
|
|
* |
50
|
|
|
* @var ConfigEditor |
51
|
|
|
*/ |
52
|
|
|
private $_configEditor; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Process factory. |
56
|
|
|
* |
57
|
|
|
* @var IProcessFactory |
58
|
|
|
*/ |
59
|
|
|
private $_processFactory; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Console IO. |
63
|
|
|
* |
64
|
|
|
* @var ConsoleIO |
65
|
|
|
*/ |
66
|
|
|
private $_io; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Cache manager. |
70
|
|
|
* |
71
|
|
|
* @var CacheManager |
72
|
|
|
*/ |
73
|
|
|
private $_cacheManager; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Revision list parser. |
77
|
|
|
* |
78
|
|
|
* @var RevisionListParser |
79
|
|
|
*/ |
80
|
|
|
private $_revisionListParser; |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Path to an svn command. |
84
|
|
|
* |
85
|
|
|
* @var string |
86
|
|
|
*/ |
87
|
|
|
private $_svnCommand = 'svn'; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Cache duration for next invoked command. |
91
|
|
|
* |
92
|
|
|
* @var mixed |
93
|
|
|
*/ |
94
|
|
|
private $_nextCommandCacheDuration = null; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Whatever to cache last repository revision or not. |
98
|
|
|
* |
99
|
|
|
* @var mixed |
100
|
|
|
*/ |
101
|
|
|
private $_lastRevisionCacheDuration = null; |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Creates repository connector. |
105
|
|
|
* |
106
|
|
|
* @param ConfigEditor $config_editor ConfigEditor. |
107
|
|
|
* @param IProcessFactory $process_factory Process factory. |
108
|
|
|
* @param ConsoleIO $io Console IO. |
109
|
|
|
* @param CacheManager $cache_manager Cache manager. |
110
|
|
|
* @param RevisionListParser $revision_list_parser Revision list parser. |
111
|
|
|
*/ |
112
|
102 |
|
public function __construct( |
113
|
|
|
ConfigEditor $config_editor, |
114
|
|
|
IProcessFactory $process_factory, |
115
|
|
|
ConsoleIO $io, |
116
|
|
|
CacheManager $cache_manager, |
117
|
|
|
RevisionListParser $revision_list_parser |
118
|
|
|
) { |
119
|
102 |
|
$this->_configEditor = $config_editor; |
120
|
102 |
|
$this->_processFactory = $process_factory; |
121
|
102 |
|
$this->_io = $io; |
122
|
102 |
|
$this->_cacheManager = $cache_manager; |
123
|
102 |
|
$this->_revisionListParser = $revision_list_parser; |
124
|
|
|
|
125
|
102 |
|
$cache_duration = $this->_configEditor->get('repository-connector.last-revision-cache-duration'); |
126
|
|
|
|
127
|
102 |
|
if ( (string)$cache_duration === '' || substr($cache_duration, 0, 1) === '0' ) { |
128
|
4 |
|
$cache_duration = 0; |
129
|
|
|
} |
130
|
|
|
|
131
|
102 |
|
$this->_lastRevisionCacheDuration = $cache_duration; |
132
|
|
|
|
133
|
102 |
|
$this->prepareSvnCommand(); |
134
|
102 |
|
} |
135
|
|
|
|
136
|
|
|
/** |
137
|
|
|
* Prepares static part of svn command to be used across the script. |
138
|
|
|
* |
139
|
|
|
* @return void |
140
|
|
|
*/ |
141
|
102 |
|
protected function prepareSvnCommand() |
142
|
|
|
{ |
143
|
102 |
|
$username = $this->_configEditor->get('repository-connector.username'); |
144
|
102 |
|
$password = $this->_configEditor->get('repository-connector.password'); |
145
|
|
|
|
146
|
102 |
|
$this->_svnCommand .= ' --non-interactive'; |
147
|
|
|
|
148
|
102 |
|
if ( $username ) { |
149
|
17 |
|
$this->_svnCommand .= ' --username ' . $username; |
150
|
|
|
} |
151
|
|
|
|
152
|
102 |
|
if ( $password ) { |
153
|
17 |
|
$this->_svnCommand .= ' --password ' . $password; |
154
|
|
|
} |
155
|
102 |
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* Builds a command. |
159
|
|
|
* |
160
|
|
|
* @param string $sub_command Sub command. |
161
|
|
|
* @param string|null $param_string Parameter string. |
162
|
|
|
* |
163
|
|
|
* @return Command |
164
|
|
|
*/ |
165
|
56 |
|
public function getCommand($sub_command, $param_string = null) |
166
|
|
|
{ |
167
|
56 |
|
$command_line = $this->buildCommand($sub_command, $param_string); |
168
|
|
|
|
169
|
55 |
|
$command = new Command( |
170
|
55 |
|
$command_line, |
171
|
55 |
|
$this->_io, |
172
|
55 |
|
$this->_cacheManager, |
173
|
55 |
|
$this->_processFactory |
174
|
|
|
); |
175
|
|
|
|
176
|
55 |
|
if ( isset($this->_nextCommandCacheDuration) ) { |
177
|
25 |
|
$command->setCacheDuration($this->_nextCommandCacheDuration); |
178
|
25 |
|
$this->_nextCommandCacheDuration = null; |
179
|
|
|
} |
180
|
|
|
|
181
|
55 |
|
return $command; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* Builds command from given arguments. |
186
|
|
|
* |
187
|
|
|
* @param string $sub_command Command. |
188
|
|
|
* @param string $param_string Parameter string. |
189
|
|
|
* |
190
|
|
|
* @return string |
191
|
|
|
* @throws \InvalidArgumentException When command contains spaces. |
192
|
|
|
*/ |
193
|
56 |
|
protected function buildCommand($sub_command, $param_string = null) |
194
|
|
|
{ |
195
|
56 |
|
if ( strpos($sub_command, ' ') !== false ) { |
196
|
1 |
|
throw new \InvalidArgumentException('The "' . $sub_command . '" sub-command contains spaces.'); |
197
|
|
|
} |
198
|
|
|
|
199
|
55 |
|
$command_line = $this->_svnCommand; |
200
|
|
|
|
201
|
55 |
|
if ( !empty($sub_command) ) { |
202
|
50 |
|
$command_line .= ' ' . $sub_command; |
203
|
|
|
} |
204
|
|
|
|
205
|
55 |
|
if ( !empty($param_string) ) { |
206
|
53 |
|
$command_line .= ' ' . $param_string; |
207
|
|
|
} |
208
|
|
|
|
209
|
55 |
|
$command_line = preg_replace_callback( |
210
|
55 |
|
'/\{([^\}]*)\}/', |
211
|
55 |
|
function (array $matches) { |
212
|
47 |
|
return escapeshellarg($matches[1]); |
213
|
55 |
|
}, |
214
|
55 |
|
$command_line |
215
|
|
|
); |
216
|
|
|
|
217
|
55 |
|
return $command_line; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Sets cache configuration for next created command. |
222
|
|
|
* |
223
|
|
|
* @param mixed $cache_duration Cache duration. |
224
|
|
|
* |
225
|
|
|
* @return self |
226
|
|
|
*/ |
227
|
26 |
|
public function withCache($cache_duration) |
228
|
|
|
{ |
229
|
26 |
|
$this->_nextCommandCacheDuration = $cache_duration; |
230
|
|
|
|
231
|
26 |
|
return $this; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Returns property value. |
236
|
|
|
* |
237
|
|
|
* @param string $name Property name. |
238
|
|
|
* @param string $path_or_url Path to get property from. |
239
|
|
|
* @param mixed $revision Revision. |
240
|
|
|
* |
241
|
|
|
* @return string |
242
|
|
|
* @throws RepositoryCommandException When other, then missing property exception happens. |
243
|
|
|
*/ |
244
|
8 |
|
public function getProperty($name, $path_or_url, $revision = null) |
245
|
|
|
{ |
246
|
8 |
|
$param_string = $name . ' {' . $path_or_url . '}'; |
247
|
|
|
|
248
|
8 |
|
if ( isset($revision) ) { |
249
|
7 |
|
$param_string .= ' --revision ' . $revision; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
// The "null" for non-existing properties is never returned, because output is converted to string. |
253
|
8 |
|
$property_value = ''; |
254
|
|
|
|
255
|
|
|
try { |
256
|
8 |
|
$property_value = $this->getCommand('propget', $param_string)->run(); |
257
|
|
|
} |
258
|
1 |
|
catch ( RepositoryCommandException $e ) { |
259
|
|
|
// Preserve SVN 1.8- behavior, where reading value of non-existing property returned an empty string. |
260
|
1 |
|
if ( $e->getCode() !== RepositoryCommandException::SVN_ERR_BASE ) { |
261
|
|
|
throw $e; |
262
|
|
|
} |
263
|
|
|
} |
264
|
|
|
|
265
|
8 |
|
return $property_value; |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* Returns relative path of given path/url to the root of the repository. |
270
|
|
|
* |
271
|
|
|
* @param string $path_or_url Path or url. |
272
|
|
|
* |
273
|
|
|
* @return string |
274
|
|
|
*/ |
275
|
3 |
|
public function getRelativePath($path_or_url) |
276
|
|
|
{ |
277
|
3 |
|
$svn_info_entry = $this->_getSvnInfoEntry($path_or_url, self::SVN_INFO_CACHE_DURATION); |
278
|
|
|
|
279
|
3 |
|
return preg_replace( |
280
|
3 |
|
'/^' . preg_quote($svn_info_entry->repository->root, '/') . '/', |
281
|
3 |
|
'', |
282
|
3 |
|
(string)$svn_info_entry->url, |
283
|
3 |
|
1 |
284
|
|
|
); |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* Returns repository root url from given path/url. |
289
|
|
|
* |
290
|
|
|
* @param string $path_or_url Path or url. |
291
|
|
|
* |
292
|
|
|
* @return string |
293
|
|
|
*/ |
294
|
3 |
|
public function getRootUrl($path_or_url) |
295
|
|
|
{ |
296
|
3 |
|
return (string)$this->_getSvnInfoEntry($path_or_url, self::SVN_INFO_CACHE_DURATION)->repository->root; |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
/** |
300
|
|
|
* Determines if path is a root of the ref. |
301
|
|
|
* |
302
|
|
|
* @param string $path Path to a file. |
303
|
|
|
* |
304
|
|
|
* @return boolean |
305
|
|
|
*/ |
306
|
13 |
|
public function isRefRoot($path) |
307
|
|
|
{ |
308
|
13 |
|
$ref = $this->getRefByPath($path); |
309
|
|
|
|
310
|
13 |
|
if ( $ref === false ) { |
311
|
4 |
|
return false; |
312
|
|
|
} |
313
|
|
|
|
314
|
9 |
|
return preg_match('#/' . preg_quote($ref, '#') . '/$#', $path) > 0; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* Detects ref from given path. |
319
|
|
|
* |
320
|
|
|
* @param string $path Path to a file. |
321
|
|
|
* |
322
|
|
|
* @return string|boolean |
323
|
|
|
* @see getProjectUrl |
324
|
|
|
*/ |
325
|
22 |
|
public function getRefByPath($path) |
326
|
|
|
{ |
327
|
22 |
|
if ( preg_match('#^.*?/(trunk|branches/[^/]+|tags/[^/]+|releases/[^/]+).*$#', $path, $regs) ) { |
328
|
14 |
|
return $regs[1]; |
329
|
|
|
} |
330
|
|
|
|
331
|
8 |
|
return false; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
/** |
335
|
|
|
* Returns URL of the working copy. |
336
|
|
|
* |
337
|
|
|
* @param string $wc_path Working copy path. |
338
|
|
|
* |
339
|
|
|
* @return string |
340
|
|
|
* @throws RepositoryCommandException When repository command failed to execute. |
341
|
|
|
*/ |
342
|
11 |
|
public function getWorkingCopyUrl($wc_path) |
343
|
|
|
{ |
344
|
11 |
|
if ( $this->isUrl($wc_path) ) { |
345
|
2 |
|
return $this->removeCredentials($wc_path); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
try { |
349
|
|
|
// TODO: No exception is thrown, when we have a valid cache, but SVN client was upgraded. |
350
|
9 |
|
$wc_url = (string)$this->_getSvnInfoEntry($wc_path, self::SVN_INFO_CACHE_DURATION)->url; |
351
|
|
|
} |
352
|
4 |
|
catch ( RepositoryCommandException $e ) { |
353
|
3 |
|
if ( $e->getCode() == RepositoryCommandException::SVN_ERR_WC_UPGRADE_REQUIRED ) { |
354
|
2 |
|
$message = explode(PHP_EOL, $e->getMessage()); |
355
|
|
|
|
356
|
2 |
|
$this->_io->writeln(array('', '<error>' . end($message) . '</error>', '')); |
357
|
|
|
|
358
|
2 |
|
if ( $this->_io->askConfirmation('Run "svn upgrade"', false) ) { |
359
|
1 |
|
$this->getCommand('upgrade', '{' . $wc_path . '}')->runLive(); |
360
|
|
|
|
361
|
1 |
|
return $this->getWorkingCopyUrl($wc_path); |
362
|
|
|
} |
363
|
|
|
} |
364
|
|
|
|
365
|
2 |
|
throw $e; |
366
|
|
|
} |
367
|
|
|
|
368
|
6 |
|
return $wc_url; |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* Returns last changed revision on path/url. |
373
|
|
|
* |
374
|
|
|
* @param string $path_or_url Path or url. |
375
|
|
|
* |
376
|
|
|
* @return integer |
377
|
|
|
*/ |
378
|
9 |
|
public function getLastRevision($path_or_url) |
379
|
|
|
{ |
380
|
|
|
// Cache "svn info" commands to remote urls, not the working copy. |
381
|
9 |
|
$cache_duration = $this->isUrl($path_or_url) ? $this->_lastRevisionCacheDuration : null; |
382
|
|
|
|
383
|
9 |
|
return (int)$this->_getSvnInfoEntry($path_or_url, $cache_duration)->commit['revision']; |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
/** |
387
|
|
|
* Determines if given path is in fact an url. |
388
|
|
|
* |
389
|
|
|
* @param string $path Path. |
390
|
|
|
* |
391
|
|
|
* @return boolean |
392
|
|
|
*/ |
393
|
29 |
|
public function isUrl($path) |
394
|
|
|
{ |
395
|
29 |
|
return strpos($path, '://') !== false; |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* Removes credentials from url. |
400
|
|
|
* |
401
|
|
|
* @param string $url URL. |
402
|
|
|
* |
403
|
|
|
* @return string |
404
|
|
|
* @throws \InvalidArgumentException When non-url given. |
405
|
|
|
*/ |
406
|
17 |
|
public function removeCredentials($url) |
407
|
|
|
{ |
408
|
17 |
|
if ( !$this->isUrl($url) ) { |
409
|
1 |
|
throw new \InvalidArgumentException('Unable to remove credentials from "' . $url . '" path.'); |
410
|
|
|
} |
411
|
|
|
|
412
|
16 |
|
return preg_replace('#^(.*)://(.*)@(.*)$#', '$1://$3', $url); |
413
|
|
|
} |
414
|
|
|
|
415
|
|
|
/** |
416
|
|
|
* Returns project url (container for "trunk/branches/tags/releases" folders). |
417
|
|
|
* |
418
|
|
|
* @param string $repository_url Repository url. |
419
|
|
|
* |
420
|
|
|
* @return string |
421
|
|
|
* @see getRefByPath |
422
|
|
|
*/ |
423
|
9 |
|
public function getProjectUrl($repository_url) |
424
|
|
|
{ |
425
|
9 |
|
if ( preg_match('#^(.*?)/(trunk|branches|tags|releases).*$#', $repository_url, $regs) ) { |
426
|
8 |
|
return $regs[1]; |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
// When known folder structure not detected consider, that project url was already given. |
430
|
1 |
|
return $repository_url; |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
/** |
434
|
|
|
* Returns "svn info" entry for path or url. |
435
|
|
|
* |
436
|
|
|
* @param string $path_or_url Path or url. |
437
|
|
|
* @param mixed $cache_duration Cache duration. |
438
|
|
|
* |
439
|
|
|
* @return \SimpleXMLElement |
440
|
|
|
* @throws \LogicException When unexpected 'svn info' results retrieved. |
441
|
|
|
*/ |
442
|
24 |
|
private function _getSvnInfoEntry($path_or_url, $cache_duration = null) |
443
|
|
|
{ |
444
|
|
|
// Cache "svn info" commands to remote urls, not the working copy. |
445
|
24 |
|
if ( !isset($cache_duration) && $this->isUrl($path_or_url) ) { |
446
|
|
|
$cache_duration = self::SVN_INFO_CACHE_DURATION; |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
// Remove credentials from url, because "svn info" fails, when used on repository root. |
450
|
24 |
|
if ( $this->isUrl($path_or_url) ) { |
451
|
12 |
|
$path_or_url = $this->removeCredentials($path_or_url); |
452
|
|
|
} |
453
|
|
|
|
454
|
|
|
// Escape "@" in path names, because peg revision syntax (path@revision) isn't used in here. |
455
|
24 |
|
$path_or_url_escaped = $path_or_url; |
456
|
|
|
|
457
|
24 |
|
if ( strpos($path_or_url, '@') !== false ) { |
458
|
1 |
|
$path_or_url_escaped .= '@'; |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
// TODO: When wc path (not url) is given, then credentials can be present in "svn info" result anyway. |
462
|
|
|
$svn_info = $this |
463
|
24 |
|
->withCache($cache_duration) |
464
|
24 |
|
->getCommand('info', '--xml {' . $path_or_url_escaped . '}') |
465
|
24 |
|
->run(); |
466
|
|
|
|
467
|
|
|
// When getting remote "svn info", then path is last folder only. |
468
|
22 |
|
$svn_info_path = (string)$svn_info->entry['path']; |
469
|
|
|
|
470
|
|
|
// In SVN 1.7+, when doing "svn info" on repository root url. |
471
|
22 |
|
if ( $svn_info_path === '.' ) { |
472
|
1 |
|
$svn_info_path = $path_or_url; |
473
|
|
|
} |
474
|
|
|
|
475
|
22 |
|
if ( basename($svn_info_path) != basename($path_or_url) ) { |
476
|
1 |
|
throw new \LogicException('The directory "' . $path_or_url . '" not found in "svn info" command results.'); |
477
|
|
|
} |
478
|
|
|
|
479
|
21 |
|
return $svn_info->entry; |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
/** |
483
|
|
|
* Returns revision, when path was added to repository. |
484
|
|
|
* |
485
|
|
|
* @param string $url Url. |
486
|
|
|
* |
487
|
|
|
* @return integer |
488
|
|
|
* @throws \InvalidArgumentException When not an url was given. |
489
|
|
|
*/ |
490
|
|
|
public function getFirstRevision($url) |
491
|
|
|
{ |
492
|
|
|
if ( !$this->isUrl($url) ) { |
493
|
|
|
throw new \InvalidArgumentException('The repository URL "' . $url . '" is invalid.'); |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
$log = $this->withCache('1 year')->getCommand('log', ' -r 1:HEAD --limit 1 --xml {' . $url . '}')->run(); |
497
|
|
|
|
498
|
|
|
return (int)$log->logentry['revision']; |
499
|
|
|
} |
500
|
|
|
|
501
|
|
|
/** |
502
|
|
|
* Returns conflicts in working copy. |
503
|
|
|
* |
504
|
|
|
* @param string $wc_path Working copy path. |
505
|
|
|
* |
506
|
|
|
* @return array |
507
|
|
|
*/ |
508
|
2 |
|
public function getWorkingCopyConflicts($wc_path) |
509
|
|
|
{ |
510
|
2 |
|
$ret = array(); |
511
|
|
|
|
512
|
2 |
|
foreach ( $this->getWorkingCopyStatus($wc_path) as $path => $status ) { |
513
|
2 |
|
if ( $this->isWorkingCopyPathStatus($status, self::STATUS_CONFLICTED) ) { |
514
|
2 |
|
$ret[] = $path; |
515
|
|
|
} |
516
|
|
|
} |
517
|
|
|
|
518
|
2 |
|
return $ret; |
519
|
|
|
} |
520
|
|
|
|
521
|
|
|
/** |
522
|
|
|
* Returns missing paths in working copy. |
523
|
|
|
* |
524
|
|
|
* @param string $wc_path Working copy path. |
525
|
|
|
* |
526
|
|
|
* @return array |
527
|
|
|
*/ |
528
|
2 |
|
public function getWorkingCopyMissing($wc_path) |
529
|
|
|
{ |
530
|
2 |
|
$ret = array(); |
531
|
|
|
|
532
|
2 |
|
foreach ( $this->getWorkingCopyStatus($wc_path) as $path => $status ) { |
533
|
1 |
|
if ( $this->isWorkingCopyPathStatus($status, self::STATUS_MISSING) ) { |
534
|
1 |
|
$ret[] = $path; |
535
|
|
|
} |
536
|
|
|
} |
537
|
|
|
|
538
|
2 |
|
return $ret; |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
/** |
542
|
|
|
* Returns compact working copy status. |
543
|
|
|
* |
544
|
|
|
* @param string $wc_path Working copy path. |
545
|
|
|
* @param string|null $changelist Changelist. |
546
|
|
|
* @param array $except_statuses Except statuses. |
547
|
|
|
* |
548
|
|
|
* @return string |
549
|
|
|
*/ |
550
|
|
|
public function getCompactWorkingCopyStatus( |
551
|
|
|
$wc_path, |
552
|
|
|
$changelist = null, |
553
|
|
|
array $except_statuses = array(self::STATUS_UNVERSIONED, self::STATUS_EXTERNAL) |
554
|
|
|
) { |
555
|
|
|
$ret = array(); |
556
|
|
|
|
557
|
|
|
foreach ( $this->getWorkingCopyStatus($wc_path, $changelist, $except_statuses) as $path => $status ) { |
558
|
|
|
$line = $this->getShortItemStatus($status['item']); // Path status. |
559
|
|
|
$line .= $this->getShortPropertiesStatus($status['props']); // Properties status. |
560
|
|
|
$line .= ' '; // Locked status. |
561
|
|
|
$line .= $status['copied'] === true ? '+' : ' '; // Copied status. |
562
|
|
|
$line .= ' ' . $path; |
563
|
|
|
|
564
|
|
|
$ret[] = $line; |
565
|
|
|
} |
566
|
|
|
|
567
|
|
|
return implode(PHP_EOL, $ret); |
568
|
|
|
} |
569
|
|
|
|
570
|
|
|
/** |
571
|
|
|
* Returns short item status. |
572
|
|
|
* |
573
|
|
|
* @param string $status Status. |
574
|
|
|
* |
575
|
|
|
* @return string |
576
|
|
|
* @throws \InvalidArgumentException When unknown status given. |
577
|
|
|
*/ |
578
|
|
|
protected function getShortItemStatus($status) |
579
|
|
|
{ |
580
|
|
|
$status_map = array( |
581
|
|
|
'added' => 'A', |
582
|
|
|
'conflicted' => 'C', |
583
|
|
|
'deleted' => 'D', |
584
|
|
|
'external' => 'X', |
585
|
|
|
'ignored' => 'I', |
586
|
|
|
// 'incomplete' => '', |
|
|
|
|
587
|
|
|
// 'merged' => '', |
|
|
|
|
588
|
|
|
'missing' => '!', |
589
|
|
|
'modified' => 'M', |
590
|
|
|
'none' => ' ', |
591
|
|
|
'normal' => '_', |
592
|
|
|
// 'obstructed' => '', |
|
|
|
|
593
|
|
|
'replaced' => 'R', |
594
|
|
|
'unversioned' => '?', |
595
|
|
|
); |
596
|
|
|
|
597
|
|
|
if ( !isset($status_map[$status]) ) { |
598
|
|
|
throw new \InvalidArgumentException('The "' . $status . '" item status is unknown.'); |
599
|
|
|
} |
600
|
|
|
|
601
|
|
|
return $status_map[$status]; |
602
|
|
|
} |
603
|
|
|
|
604
|
|
|
/** |
605
|
|
|
* Returns short item status. |
606
|
|
|
* |
607
|
|
|
* @param string $status Status. |
608
|
|
|
* |
609
|
|
|
* @return string |
610
|
|
|
* @throws \InvalidArgumentException When unknown status given. |
611
|
|
|
*/ |
612
|
|
|
protected function getShortPropertiesStatus($status) |
613
|
|
|
{ |
614
|
|
|
$status_map = array( |
615
|
|
|
'conflicted' => 'C', |
616
|
|
|
'modified' => 'M', |
617
|
|
|
'normal' => '_', |
618
|
|
|
'none' => ' ', |
619
|
|
|
); |
620
|
|
|
|
621
|
|
|
if ( !isset($status_map[$status]) ) { |
622
|
|
|
throw new \InvalidArgumentException('The "' . $status . '" properties status is unknown.'); |
623
|
|
|
} |
624
|
|
|
|
625
|
|
|
return $status_map[$status]; |
626
|
|
|
} |
627
|
|
|
|
628
|
|
|
/** |
629
|
|
|
* Returns working copy status. |
630
|
|
|
* |
631
|
|
|
* @param string $wc_path Working copy path. |
632
|
|
|
* @param string|null $changelist Changelist. |
633
|
|
|
* @param array $except_statuses Except statuses. |
634
|
|
|
* |
635
|
|
|
* @return array |
636
|
|
|
* @throws \InvalidArgumentException When changelist doens't exist. |
637
|
|
|
*/ |
638
|
10 |
|
public function getWorkingCopyStatus( |
639
|
|
|
$wc_path, |
640
|
|
|
$changelist = null, |
641
|
|
|
array $except_statuses = array(self::STATUS_UNVERSIONED, self::STATUS_EXTERNAL) |
642
|
|
|
) { |
643
|
10 |
|
$all_paths = array(); |
644
|
|
|
|
645
|
10 |
|
$status = $this->getCommand('status', '--xml {' . $wc_path . '}')->run(); |
646
|
|
|
|
647
|
10 |
|
if ( !strlen($changelist) ) { |
648
|
|
|
// Accept all entries from "target" and "changelist" nodes. |
649
|
8 |
|
foreach ( $status->children() as $entries ) { |
650
|
8 |
|
$child_name = $entries->getName(); |
651
|
|
|
|
652
|
8 |
|
if ( $child_name === 'target' || $child_name === 'changelist' ) { |
653
|
8 |
|
$all_paths += $this->processStatusEntryNodes($wc_path, $entries); |
654
|
|
|
} |
655
|
|
|
} |
656
|
|
|
} |
657
|
|
|
else { |
658
|
|
|
// Accept all entries from "changelist" node and parent folders from "target" node. |
659
|
2 |
|
foreach ( $status->changelist as $changelist_entries ) { |
660
|
2 |
|
if ( (string)$changelist_entries['name'] === $changelist ) { |
661
|
2 |
|
$all_paths += $this->processStatusEntryNodes($wc_path, $changelist_entries); |
662
|
|
|
} |
663
|
|
|
} |
664
|
|
|
|
665
|
2 |
|
if ( !$all_paths ) { |
666
|
1 |
|
throw new \InvalidArgumentException('The "' . $changelist . '" changelist doens\'t exist.'); |
667
|
|
|
} |
668
|
|
|
|
669
|
1 |
|
$parent_paths = $this->getParentPaths(array_keys($all_paths)); |
670
|
|
|
|
671
|
1 |
|
foreach ( $status->target as $target_entries ) { |
672
|
1 |
|
foreach ( $this->processStatusEntryNodes($wc_path, $target_entries) as $path => $path_data ) { |
673
|
1 |
|
if ( in_array($path, $parent_paths) ) { |
674
|
1 |
|
$all_paths[$path] = $path_data; |
675
|
|
|
} |
676
|
|
|
} |
677
|
|
|
} |
678
|
|
|
|
679
|
1 |
|
ksort($all_paths, SORT_STRING); |
680
|
|
|
} |
681
|
|
|
|
682
|
9 |
|
$changed_paths = array(); |
683
|
|
|
|
684
|
9 |
|
foreach ( $all_paths as $path => $status ) { |
685
|
|
|
// Exclude paths, that haven't changed (e.g. from changelists). |
686
|
8 |
|
if ( $this->isWorkingCopyPathStatus($status, self::STATUS_NORMAL) ) { |
687
|
6 |
|
continue; |
688
|
|
|
} |
689
|
|
|
|
690
|
|
|
// Exclude paths with requested statuses. |
691
|
8 |
|
if ( $except_statuses ) { |
692
|
7 |
|
foreach ( $except_statuses as $except_status ) { |
693
|
7 |
|
if ( $this->isWorkingCopyPathStatus($status, $except_status) ) { |
694
|
7 |
|
continue 2; |
695
|
|
|
} |
696
|
|
|
} |
697
|
|
|
} |
698
|
|
|
|
699
|
8 |
|
$changed_paths[$path] = $status; |
700
|
|
|
} |
701
|
|
|
|
702
|
9 |
|
return $changed_paths; |
703
|
|
|
} |
704
|
|
|
|
705
|
|
|
/** |
706
|
|
|
* Processes "entry" nodes from "svn status" command. |
707
|
|
|
* |
708
|
|
|
* @param string $wc_path Working copy path. |
709
|
|
|
* @param \SimpleXMLElement $entries Entries. |
710
|
|
|
* |
711
|
|
|
* @return array |
712
|
|
|
*/ |
713
|
8 |
|
protected function processStatusEntryNodes($wc_path, \SimpleXMLElement $entries) |
714
|
|
|
{ |
715
|
8 |
|
$ret = array(); |
716
|
|
|
|
717
|
8 |
|
foreach ( $entries as $entry ) { |
718
|
8 |
|
$path = (string)$entry['path']; |
719
|
8 |
|
$path = $path === $wc_path ? '.' : str_replace($wc_path . '/', '', $path); |
720
|
|
|
|
721
|
8 |
|
$ret[$path] = array( |
722
|
8 |
|
'item' => (string)$entry->{'wc-status'}['item'], |
723
|
8 |
|
'props' => (string)$entry->{'wc-status'}['props'], |
724
|
8 |
|
'tree-conflicted' => (string)$entry->{'wc-status'}['tree-conflicted'] === 'true', |
725
|
8 |
|
'copied' => (string)$entry->{'wc-status'}['copied'] === 'true', |
726
|
|
|
); |
727
|
|
|
} |
728
|
|
|
|
729
|
8 |
|
return $ret; |
730
|
|
|
} |
731
|
|
|
|
732
|
|
|
/** |
733
|
|
|
* Detects specific path status. |
734
|
|
|
* |
735
|
|
|
* @param array $status Path status. |
736
|
|
|
* @param string $path_status Expected path status. |
737
|
|
|
* |
738
|
|
|
* @return boolean |
739
|
|
|
*/ |
740
|
8 |
|
protected function isWorkingCopyPathStatus(array $status, $path_status) |
741
|
|
|
{ |
742
|
8 |
|
$tree_conflicted = $status['tree-conflicted']; |
743
|
|
|
|
744
|
8 |
|
if ( $path_status === self::STATUS_NORMAL ) { |
745
|
|
|
// Normal if all of 3 are normal. |
746
|
8 |
|
return $status['item'] === $path_status |
747
|
8 |
|
&& ($status['props'] === $path_status || $status['props'] === self::STATUS_NONE) |
748
|
8 |
|
&& !$tree_conflicted; |
749
|
|
|
} |
750
|
7 |
|
elseif ( $path_status === self::STATUS_CONFLICTED ) { |
751
|
|
|
// Conflict if any of 3 has conflict. |
752
|
2 |
|
return $status['item'] === $path_status || $status['props'] === $path_status || $tree_conflicted; |
753
|
|
|
} |
754
|
7 |
|
elseif ( $path_status === self::STATUS_UNVERSIONED ) { |
755
|
7 |
|
return $status['item'] === $path_status && $status['props'] === self::STATUS_NONE; |
756
|
|
|
} |
757
|
|
|
|
758
|
7 |
|
return $status['item'] === $path_status; |
759
|
|
|
} |
760
|
|
|
|
761
|
|
|
/** |
762
|
|
|
* Returns parent paths from given paths. |
763
|
|
|
* |
764
|
|
|
* @param array $paths Paths. |
765
|
|
|
* |
766
|
|
|
* @return array |
767
|
|
|
*/ |
768
|
1 |
|
protected function getParentPaths(array $paths) |
769
|
|
|
{ |
770
|
1 |
|
$ret = array(); |
771
|
|
|
|
772
|
1 |
|
foreach ( $paths as $path ) { |
773
|
1 |
|
while ( $path !== '.' ) { |
774
|
1 |
|
$path = dirname($path); |
775
|
1 |
|
$ret[] = $path; |
776
|
|
|
} |
777
|
|
|
} |
778
|
|
|
|
779
|
1 |
|
return array_unique($ret); |
780
|
|
|
} |
781
|
|
|
|
782
|
|
|
/** |
783
|
|
|
* Returns working copy changelists. |
784
|
|
|
* |
785
|
|
|
* @param string $wc_path Working copy path. |
786
|
|
|
* |
787
|
|
|
* @return array |
788
|
|
|
*/ |
789
|
2 |
|
public function getWorkingCopyChangelists($wc_path) |
790
|
|
|
{ |
791
|
2 |
|
$ret = array(); |
792
|
2 |
|
$status = $this->getCommand('status', '--xml {' . $wc_path . '}')->run(); |
793
|
|
|
|
794
|
2 |
|
foreach ( $status->changelist as $changelist ) { |
795
|
1 |
|
$ret[] = (string)$changelist['name']; |
796
|
|
|
} |
797
|
|
|
|
798
|
2 |
|
sort($ret, SORT_STRING); |
799
|
|
|
|
800
|
2 |
|
return $ret; |
801
|
|
|
} |
802
|
|
|
|
803
|
|
|
/** |
804
|
|
|
* Returns revisions of paths in a working copy. |
805
|
|
|
* |
806
|
|
|
* @param string $wc_path Working copy path. |
807
|
|
|
* |
808
|
|
|
* @return array |
809
|
|
|
*/ |
810
|
|
|
public function getWorkingCopyRevisions($wc_path) |
811
|
|
|
{ |
812
|
|
|
$revisions = array(); |
813
|
|
|
$status = $this->getCommand('status', '--xml --verbose {' . $wc_path . '}')->run(); |
814
|
|
|
|
815
|
|
|
foreach ( $status->target as $target ) { |
816
|
|
|
if ( (string)$target['path'] !== $wc_path ) { |
817
|
|
|
continue; |
818
|
|
|
} |
819
|
|
|
|
820
|
|
|
foreach ( $target as $entry ) { |
821
|
|
|
$revision = (int)$entry->{'wc-status'}['revision']; |
822
|
|
|
$revisions[$revision] = true; |
823
|
|
|
} |
824
|
|
|
} |
825
|
|
|
|
826
|
|
|
// The "-1" revision happens, when external is deleted. |
827
|
|
|
// The "0" happens for not committed paths (e.g. added). |
828
|
|
|
unset($revisions[-1], $revisions[0]); |
829
|
|
|
|
830
|
|
|
return array_keys($revisions); |
831
|
|
|
} |
832
|
|
|
|
833
|
|
|
/** |
834
|
|
|
* Determines if there is a working copy on a given path. |
835
|
|
|
* |
836
|
|
|
* @param string $path Path. |
837
|
|
|
* |
838
|
|
|
* @return boolean |
839
|
|
|
* @throws \InvalidArgumentException When path isn't found. |
840
|
|
|
* @throws RepositoryCommandException When repository command failed to execute. |
841
|
|
|
*/ |
842
|
|
|
public function isWorkingCopy($path) |
843
|
|
|
{ |
844
|
|
|
if ( $this->isUrl($path) || !file_exists($path) ) { |
845
|
|
|
throw new \InvalidArgumentException('Path "' . $path . '" not found.'); |
846
|
|
|
} |
847
|
|
|
|
848
|
|
|
try { |
849
|
|
|
$wc_url = $this->getWorkingCopyUrl($path); |
850
|
|
|
} |
851
|
|
|
catch ( RepositoryCommandException $e ) { |
852
|
|
|
if ( $e->getCode() == RepositoryCommandException::SVN_ERR_WC_NOT_WORKING_COPY ) { |
853
|
|
|
return false; |
854
|
|
|
} |
855
|
|
|
|
856
|
|
|
throw $e; |
857
|
|
|
} |
858
|
|
|
|
859
|
|
|
return $wc_url != ''; |
860
|
|
|
} |
861
|
|
|
|
862
|
|
|
/** |
863
|
|
|
* Returns list of add/removed revisions from last merge operation. |
864
|
|
|
* |
865
|
|
|
* @param string $wc_path Working copy path, where merge happens. |
866
|
|
|
* @param boolean $regular_or_reverse Merge direction ("regular" or "reverse"). |
867
|
|
|
* |
868
|
|
|
* @return array |
869
|
|
|
*/ |
870
|
4 |
|
public function getMergedRevisionChanges($wc_path, $regular_or_reverse) |
871
|
|
|
{ |
872
|
4 |
|
$final_paths = array(); |
873
|
|
|
|
874
|
4 |
|
if ( $regular_or_reverse ) { |
875
|
2 |
|
$old_paths = $this->getMergedRevisions($wc_path, 'BASE'); |
876
|
2 |
|
$new_paths = $this->getMergedRevisions($wc_path); |
877
|
|
|
} |
878
|
|
|
else { |
879
|
2 |
|
$old_paths = $this->getMergedRevisions($wc_path); |
880
|
2 |
|
$new_paths = $this->getMergedRevisions($wc_path, 'BASE'); |
881
|
|
|
} |
882
|
|
|
|
883
|
4 |
|
if ( $old_paths === $new_paths ) { |
884
|
2 |
|
return array(); |
885
|
|
|
} |
886
|
|
|
|
887
|
2 |
|
foreach ( $new_paths as $new_path => $new_merged_revisions ) { |
888
|
2 |
|
if ( !isset($old_paths[$new_path]) ) { |
889
|
|
|
// Merge from new path. |
890
|
2 |
|
$final_paths[$new_path] = $this->_revisionListParser->expandRanges( |
891
|
2 |
|
explode(',', $new_merged_revisions) |
892
|
|
|
); |
893
|
|
|
} |
894
|
2 |
|
elseif ( $new_merged_revisions != $old_paths[$new_path] ) { |
895
|
|
|
// Merge on existing path. |
896
|
2 |
|
$new_merged_revisions_parsed = $this->_revisionListParser->expandRanges( |
897
|
2 |
|
explode(',', $new_merged_revisions) |
898
|
|
|
); |
899
|
2 |
|
$old_merged_revisions_parsed = $this->_revisionListParser->expandRanges( |
900
|
2 |
|
explode(',', $old_paths[$new_path]) |
901
|
|
|
); |
902
|
2 |
|
$final_paths[$new_path] = array_values( |
903
|
2 |
|
array_diff($new_merged_revisions_parsed, $old_merged_revisions_parsed) |
904
|
|
|
); |
905
|
|
|
} |
906
|
|
|
} |
907
|
|
|
|
908
|
2 |
|
return $final_paths; |
909
|
|
|
} |
910
|
|
|
|
911
|
|
|
/** |
912
|
|
|
* Returns list of merged revisions per path. |
913
|
|
|
* |
914
|
|
|
* @param string $wc_path Merge target: working copy path. |
915
|
|
|
* @param integer $revision Revision. |
916
|
|
|
* |
917
|
|
|
* @return array |
918
|
|
|
*/ |
919
|
4 |
|
protected function getMergedRevisions($wc_path, $revision = null) |
920
|
|
|
{ |
921
|
4 |
|
$paths = array(); |
922
|
|
|
|
923
|
4 |
|
$merge_info = $this->getProperty('svn:mergeinfo', $wc_path, $revision); |
924
|
4 |
|
$merge_info = array_filter(explode("\n", $merge_info)); |
925
|
|
|
|
926
|
4 |
|
foreach ( $merge_info as $merge_info_line ) { |
927
|
4 |
|
list($path, $revisions) = explode(':', $merge_info_line, 2); |
928
|
4 |
|
$paths[$path] = $revisions; |
929
|
|
|
} |
930
|
|
|
|
931
|
4 |
|
return $paths; |
932
|
|
|
} |
933
|
|
|
|
934
|
|
|
/** |
935
|
|
|
* Returns file contents at given revision. |
936
|
|
|
* |
937
|
|
|
* @param string $path_or_url Path or url. |
938
|
|
|
* @param integer $revision Revision. |
939
|
|
|
* |
940
|
|
|
* @return string |
941
|
|
|
*/ |
942
|
1 |
|
public function getFileContent($path_or_url, $revision) |
943
|
|
|
{ |
944
|
|
|
return $this |
945
|
1 |
|
->withCache(self::SVN_CAT_CACHE_DURATION) |
946
|
1 |
|
->getCommand('cat', '{' . $path_or_url . '} --revision ' . $revision) |
947
|
1 |
|
->run(); |
948
|
|
|
} |
949
|
|
|
|
950
|
|
|
} |
951
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.