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 ) { |
||||
0 ignored issues
–
show
|
|||||
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.' |
||||
0 ignored issues
–
show
Are you sure
$sub_command of type string|string[] can be used in concatenation ?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
182 | ); |
||||
183 | } |
||||
184 | |||||
185 | $this->runSubCommand($sub_command); |
||||
0 ignored issues
–
show
It seems like
$sub_command can also be of type string[] ; however, parameter $sub_command_name of ConsoleHelpers\SVNBuddy\...ommand::runSubCommand() 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
![]() |
|||||
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); |
||||
0 ignored issues
–
show
Are you sure
$raw_ignore_add of type boolean|string|string[] can be used in concatenation ?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
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); |
||||
0 ignored issues
–
show
Are you sure
$raw_ignore_remove of type boolean|string|string[] can be used in concatenation ?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
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 ) { |
||||
0 ignored issues
–
show
The expression
$ignored 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 ![]() |
|||||
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 ) { |
||||
0 ignored issues
–
show
The expression
$working_copies 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 ![]() |
|||||
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.