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\Command; |
12
|
|
|
|
13
|
|
|
|
14
|
|
|
use ConsoleHelpers\ConsoleKit\Exception\CommandException; |
15
|
|
|
use ConsoleHelpers\SVNBuddy\Config\AbstractConfigSetting; |
16
|
|
|
use ConsoleHelpers\SVNBuddy\Config\PathsConfigSetting; |
17
|
|
|
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; |
18
|
|
|
use Symfony\Component\Console\Application; |
19
|
|
|
use Symfony\Component\Console\Command\Command; |
20
|
|
|
use Symfony\Component\Console\Input\InputArgument; |
21
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
22
|
|
|
use Symfony\Component\Console\Input\InputOption; |
23
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
24
|
|
|
|
25
|
|
|
class AggregateCommand extends AbstractCommand implements IConfigAwareCommand |
26
|
|
|
{ |
27
|
|
|
|
28
|
|
|
const SETTING_AGGREGATE_IGNORE = 'aggregate.ignore'; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* {@inheritdoc} |
32
|
|
|
*/ |
33
|
|
|
protected function configure() |
34
|
|
|
{ |
35
|
|
|
$this |
36
|
|
|
->setName('aggregate') |
37
|
|
|
->setDescription( |
38
|
|
|
'Runs other command sequentially on every working copy on a path' |
39
|
|
|
) |
40
|
|
|
->addArgument( |
41
|
|
|
'sub-command', |
42
|
|
|
InputArgument::OPTIONAL, |
43
|
|
|
'Command to execute on each found working copy' |
44
|
|
|
) |
45
|
|
|
->addArgument( |
46
|
|
|
'path', |
47
|
|
|
InputArgument::OPTIONAL, |
48
|
|
|
'Path to folder with working copies', |
49
|
|
|
'.' |
50
|
|
|
) |
51
|
|
|
->addOption( |
52
|
|
|
'ignore-add', |
53
|
|
|
null, |
54
|
|
|
InputOption::VALUE_REQUIRED, |
55
|
|
|
'Adds path to ignored directory list' |
56
|
|
|
) |
57
|
|
|
->addOption( |
58
|
|
|
'ignore-remove', |
59
|
|
|
null, |
60
|
|
|
InputOption::VALUE_REQUIRED, |
61
|
|
|
'Removes path to ignored directory list' |
62
|
|
|
) |
63
|
|
|
->addOption( |
64
|
|
|
'ignore-show', |
65
|
|
|
null, |
66
|
|
|
InputOption::VALUE_NONE, |
67
|
|
|
'Show ignored directory list' |
68
|
|
|
) |
69
|
|
|
->addOption( |
70
|
|
|
'recursive', |
71
|
|
|
null, |
72
|
|
|
InputOption::VALUE_NONE, |
73
|
|
|
'Perform deep scan for working copies' |
74
|
|
|
); |
75
|
|
|
|
76
|
|
|
parent::configure(); |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Copies relevant options from supported sub-commands. |
81
|
|
|
* |
82
|
|
|
* @param Application $application An Application instance. |
83
|
|
|
* |
84
|
|
|
* @return void |
85
|
|
|
*/ |
86
|
|
|
public function setApplication(Application $application = null) |
87
|
|
|
{ |
88
|
|
|
parent::setApplication($application); |
89
|
|
|
|
90
|
|
|
// No application is provided, when this command is disabled. |
91
|
|
|
if ( !$application ) { |
92
|
|
|
return; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
$input_definition = $this->getDefinition(); |
96
|
|
|
|
97
|
|
|
foreach ( $this->getSubCommands() as $sub_command ) { |
98
|
|
|
assert($sub_command instanceof IAggregatorAwareCommand); |
99
|
|
|
$copy_options = $sub_command->getAggregatedOptions(); |
100
|
|
|
|
101
|
|
|
if ( !$copy_options ) { |
|
|
|
|
102
|
|
|
continue; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
$sub_command_input_definition = $sub_command->getDefinition(); |
106
|
|
|
|
107
|
|
|
foreach ( $copy_options as $copy_option_name ) { |
108
|
|
|
$copy_option = $sub_command_input_definition->getOption($copy_option_name); |
109
|
|
|
$input_definition->addOption($copy_option); |
110
|
|
|
} |
111
|
|
|
} |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Return possible values for the named argument |
116
|
|
|
* |
117
|
|
|
* @param string $argumentName Argument name. |
118
|
|
|
* @param CompletionContext $context Completion context. |
119
|
|
|
* |
120
|
|
|
* @return array |
121
|
|
|
*/ |
122
|
|
|
public function completeArgumentValues($argumentName, CompletionContext $context) |
123
|
|
|
{ |
124
|
|
|
$ret = parent::completeArgumentValues($argumentName, $context); |
125
|
|
|
|
126
|
|
|
if ( $argumentName === 'sub-command' ) { |
127
|
|
|
return $this->getSubCommandNames(); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
return $ret; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Returns available sub command names. |
135
|
|
|
* |
136
|
|
|
* @return array |
137
|
|
|
*/ |
138
|
|
|
protected function getSubCommandNames() |
139
|
|
|
{ |
140
|
|
|
return array_keys($this->getSubCommands()); |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* Returns available sub commands. |
145
|
|
|
* |
146
|
|
|
* @return Command[] |
147
|
|
|
*/ |
148
|
|
|
protected function getSubCommands() |
149
|
|
|
{ |
150
|
|
|
$ret = array(); |
151
|
|
|
|
152
|
|
|
foreach ( $this->getApplication()->all() as $alias => $command ) { |
153
|
|
|
if ( $command instanceof IAggregatorAwareCommand ) { |
154
|
|
|
$ret[$alias] = $command; |
155
|
|
|
} |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
return $ret; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* {@inheritdoc} |
163
|
|
|
* |
164
|
|
|
* @throws \RuntimeException When "sub-command" argument not specified. |
165
|
|
|
* @throws \RuntimeException When specified sub-command doesn't support aggregation. |
166
|
|
|
*/ |
167
|
|
|
protected function execute(InputInterface $input, OutputInterface $output) |
168
|
|
|
{ |
169
|
|
|
if ( $this->processIgnoreAdd() || $this->processIgnoreRemove() || $this->processIgnoreShow() ) { |
170
|
|
|
return; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
$sub_command = $this->io->getArgument('sub-command'); |
174
|
|
|
|
175
|
|
|
if ( $sub_command === null ) { |
176
|
|
|
throw new \RuntimeException('Not enough arguments (missing: "sub-command").'); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
if ( !in_array($sub_command, $this->getSubCommandNames()) ) { |
180
|
|
|
throw new \RuntimeException( |
181
|
|
|
'The "' . $sub_command . '" sub-command is unknown or doesn\'t support aggregation.' |
|
|
|
|
182
|
|
|
); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
$this->runSubCommand($sub_command); |
|
|
|
|
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Adds path to ignored directory list. |
190
|
|
|
* |
191
|
|
|
* @return boolean |
192
|
|
|
* @throws CommandException When directory is already ignored. |
193
|
|
|
* @throws CommandException When directory does not exist. |
194
|
|
|
*/ |
195
|
|
|
protected function processIgnoreAdd() |
196
|
|
|
{ |
197
|
|
|
$raw_ignore_add = $this->io->getOption('ignore-add'); |
198
|
|
|
|
199
|
|
|
if ( $raw_ignore_add === null ) { |
200
|
|
|
return false; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
$ignored = $this->getIgnored(); |
204
|
|
|
$ignore_add = realpath($this->getRawPath() . '/' . $raw_ignore_add); |
|
|
|
|
205
|
|
|
|
206
|
|
|
if ( $ignore_add === false ) { |
207
|
|
|
throw new CommandException('The "' . $raw_ignore_add . '" path does not exist.'); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
if ( in_array($ignore_add, $ignored) ) { |
211
|
|
|
throw new CommandException('The "' . $ignore_add . '" directory is already ignored.'); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
$ignored[] = $ignore_add; |
215
|
|
|
$this->setSetting(self::SETTING_AGGREGATE_IGNORE, $ignored); |
216
|
|
|
|
217
|
|
|
return true; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Removes path from ignored directory list. |
222
|
|
|
* |
223
|
|
|
* @return boolean |
224
|
|
|
* @throws CommandException When directory is not ignored. |
225
|
|
|
*/ |
226
|
|
|
protected function processIgnoreRemove() |
227
|
|
|
{ |
228
|
|
|
$raw_ignore_remove = $this->io->getOption('ignore-remove'); |
229
|
|
|
|
230
|
|
|
if ( $raw_ignore_remove === null ) { |
231
|
|
|
return false; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
$ignored = $this->getIgnored(); |
235
|
|
|
$ignore_remove = realpath($this->getRawPath() . '/' . $raw_ignore_remove); |
|
|
|
|
236
|
|
|
|
237
|
|
|
if ( $ignore_remove === false ) { |
238
|
|
|
throw new CommandException('The "' . $raw_ignore_remove . '" path does not exist.'); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
if ( !in_array($ignore_remove, $ignored) ) { |
242
|
|
|
throw new CommandException('The "' . $ignore_remove . '" directory is not ignored.'); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
$ignored = array_diff($ignored, array($ignore_remove)); |
246
|
|
|
$this->setSetting(self::SETTING_AGGREGATE_IGNORE, $ignored); |
247
|
|
|
|
248
|
|
|
return true; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Shows ignored paths. |
253
|
|
|
* |
254
|
|
|
* @return boolean |
255
|
|
|
*/ |
256
|
|
|
protected function processIgnoreShow() |
257
|
|
|
{ |
258
|
|
|
if ( !$this->io->getOption('ignore-show') ) { |
259
|
|
|
return false; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
$ignored = $this->getIgnored(); |
263
|
|
|
|
264
|
|
|
if ( !$ignored ) { |
|
|
|
|
265
|
|
|
$this->io->writeln('No paths found in ignored directory list.'); |
266
|
|
|
|
267
|
|
|
return true; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
$this->io->writeln(array('Paths in ignored directory list:', '')); |
271
|
|
|
|
272
|
|
|
foreach ( $ignored as $ignored_path ) { |
273
|
|
|
$this->io->writeln(' * ' . $ignored_path); |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
$this->io->writeln(''); |
277
|
|
|
|
278
|
|
|
return true; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* Returns ignored paths. |
283
|
|
|
* |
284
|
|
|
* @return array |
285
|
|
|
*/ |
286
|
|
|
protected function getIgnored() |
287
|
|
|
{ |
288
|
|
|
return $this->getSetting(self::SETTING_AGGREGATE_IGNORE); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Runs sub-command. |
293
|
|
|
* |
294
|
|
|
* @param string $sub_command_name Sub-command name. |
295
|
|
|
* |
296
|
|
|
* @return void |
297
|
|
|
* @throws \RuntimeException When command was used inside a working copy. |
298
|
|
|
*/ |
299
|
|
|
protected function runSubCommand($sub_command_name) |
300
|
|
|
{ |
301
|
|
|
$path = realpath($this->getRawPath()); |
302
|
|
|
|
303
|
|
|
if ( $this->repositoryConnector->isWorkingCopy($path) ) { |
304
|
|
|
throw new \RuntimeException('The "' . $path . '" must not be a working copy.'); |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
$working_copies = $this->getWorkingCopyPaths($path); |
308
|
|
|
$working_copy_count = count($working_copies); |
309
|
|
|
|
310
|
|
|
$percent_done = 0; |
311
|
|
|
$percent_increment = round(100 / count($working_copies), 2); |
312
|
|
|
|
313
|
|
|
$sub_command_arguments = $this->prepareSubCommandArguments($sub_command_name); |
314
|
|
|
|
315
|
|
|
foreach ( $working_copies as $index => $wc_path ) { |
316
|
|
|
$this->io->writeln(array( |
317
|
|
|
'', |
318
|
|
|
'Executing <info>' . $sub_command_name . '</info> command on <info>' . $wc_path . '</info> path', |
319
|
|
|
'', |
320
|
|
|
)); |
321
|
|
|
|
322
|
|
|
$sub_command_arguments['path'] = $wc_path; |
323
|
|
|
|
324
|
|
|
$this->runOtherCommand($sub_command_name, $sub_command_arguments); |
325
|
|
|
|
326
|
|
|
$this->io->writeln( |
327
|
|
|
'<info>' . ($index + 1) . ' of ' . $working_copy_count . ' sub-commands completed.</info>' |
328
|
|
|
); |
329
|
|
|
$percent_done += $percent_increment; |
330
|
|
|
} |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* Prepares sub-command arguments. |
335
|
|
|
* |
336
|
|
|
* @param string $sub_command_name Sub-command name. |
337
|
|
|
* |
338
|
|
|
* @return array |
339
|
|
|
*/ |
340
|
|
|
protected function prepareSubCommandArguments($sub_command_name) |
341
|
|
|
{ |
342
|
|
|
$sub_command_arguments = array('path' => ''); |
343
|
|
|
|
344
|
|
|
$sub_command = $this->getApplication()->get($sub_command_name); |
345
|
|
|
assert($sub_command instanceof IAggregatorAwareCommand); |
346
|
|
|
|
347
|
|
|
foreach ( $sub_command->getAggregatedOptions() as $copy_option_name ) { |
348
|
|
|
$copy_option_value = $this->io->getOption($copy_option_name); |
349
|
|
|
|
350
|
|
|
if ( $copy_option_value ) { |
351
|
|
|
$sub_command_arguments['--' . $copy_option_name] = $copy_option_value; |
352
|
|
|
} |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
return $sub_command_arguments; |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* Returns working copies found at given path. |
360
|
|
|
* |
361
|
|
|
* @param string $path Path. |
362
|
|
|
* |
363
|
|
|
* @return array |
364
|
|
|
* @throws CommandException When no working copies where found. |
365
|
|
|
*/ |
366
|
|
|
protected function getWorkingCopyPaths($path) |
367
|
|
|
{ |
368
|
|
|
$this->io->write('Looking for working copies ... '); |
369
|
|
|
$all_working_copies = $this->getWorkingCopiesRecursive($path); |
370
|
|
|
$working_copies = array_diff($all_working_copies, $this->getIgnored()); |
371
|
|
|
|
372
|
|
|
$all_working_copies_count = count($all_working_copies); |
373
|
|
|
$working_copies_count = count($working_copies); |
374
|
|
|
|
375
|
|
|
if ( $all_working_copies_count != $working_copies_count ) { |
376
|
|
|
$ignored_suffix = ' (' . ($all_working_copies_count - $working_copies_count) . ' ignored)'; |
377
|
|
|
} |
378
|
|
|
else { |
379
|
|
|
$ignored_suffix = ''; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
if ( !$working_copies ) { |
|
|
|
|
383
|
|
|
$this->io->writeln('<error>None found' . $ignored_suffix . '</error>'); |
384
|
|
|
|
385
|
|
|
throw new CommandException('No working copies found at "' . $path . '" path.'); |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
$this->io->writeln('<info>' . $working_copies_count . ' found' . $ignored_suffix . '</info>'); |
389
|
|
|
|
390
|
|
|
return array_values($working_copies); |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
/** |
394
|
|
|
* Returns working copy locations recursively. |
395
|
|
|
* |
396
|
|
|
* @param string $path Path. |
397
|
|
|
* |
398
|
|
|
* @return array |
399
|
|
|
*/ |
400
|
|
|
protected function getWorkingCopiesRecursive($path) |
401
|
|
|
{ |
402
|
|
|
$working_copies = array(); |
403
|
|
|
|
404
|
|
|
if ( $this->io->isVerbose() ) { |
405
|
|
|
$this->io->writeln( |
406
|
|
|
array('', '<debug>scanning: ' . $path . '</debug>') |
407
|
|
|
); |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
// Recursively scan for working copies. |
411
|
|
|
if ( $this->io->getOption('recursive') ) { |
412
|
|
|
foreach ( glob($path . '/*', GLOB_ONLYDIR) as $sub_folder ) { |
413
|
|
|
if ( $this->isExcludedFolder($sub_folder) ) { |
414
|
|
|
continue; |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
if ( $this->repositoryConnector->isWorkingCopy($sub_folder) ) { |
418
|
|
|
$working_copies[] = $sub_folder; |
419
|
|
|
} |
420
|
|
|
else { |
421
|
|
|
$working_copies = array_merge($working_copies, $this->getWorkingCopiesRecursive($sub_folder)); |
422
|
|
|
} |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
return $working_copies; |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
// Detect working copies only in current directly. |
429
|
|
|
foreach ( glob($path . '/*', GLOB_ONLYDIR) as $sub_folder ) { |
430
|
|
|
if ( $this->isExcludedFolder($sub_folder) ) { |
431
|
|
|
continue; |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
if ( $this->repositoryConnector->isWorkingCopy($sub_folder) ) { |
435
|
|
|
$working_copies[] = $sub_folder; |
436
|
|
|
} |
437
|
|
|
} |
438
|
|
|
|
439
|
|
|
return $working_copies; |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
/** |
443
|
|
|
* Determines if given folder should be excluded from processing. |
444
|
|
|
* |
445
|
|
|
* @param string $sub_folder Sub folder. |
446
|
|
|
* |
447
|
|
|
* @return boolean |
448
|
|
|
*/ |
449
|
|
|
protected function isExcludedFolder($sub_folder) |
450
|
|
|
{ |
451
|
|
|
return file_exists($sub_folder . '/.git') |
452
|
|
|
|| file_exists($sub_folder . '/CVS') |
453
|
|
|
|| in_array(basename($sub_folder), array('node_modules', 'vendor')); |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
/** |
457
|
|
|
* Returns list of config settings. |
458
|
|
|
* |
459
|
|
|
* @return AbstractConfigSetting[] |
460
|
|
|
*/ |
461
|
|
|
public function getConfigSettings() |
462
|
|
|
{ |
463
|
|
|
return array( |
464
|
|
|
new PathsConfigSetting(self::SETTING_AGGREGATE_IGNORE, '', AbstractConfigSetting::SCOPE_GLOBAL), |
465
|
|
|
); |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
} |
469
|
|
|
|
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.