AggregateCommand::processIgnoreRemove()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 23
ccs 0
cts 13
cp 0
rs 9.8666
cc 4
nc 4
nop 0
crap 20
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
Bug Best Practice introduced by
The expression $copy_options 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...
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
Bug introduced by
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 ignore-type  annotation

181
				'The "' . /** @scrutinizer ignore-type */ $sub_command . '" sub-command is unknown or doesn\'t support aggregation.'
Loading history...
182
			);
183
		}
184
185
		$this->runSubCommand($sub_command);
0 ignored issues
show
Bug introduced by
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 ignore-type  annotation

185
		$this->runSubCommand(/** @scrutinizer ignore-type */ $sub_command);
Loading history...
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
Bug introduced by
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 ignore-type  annotation

204
		$ignore_add = realpath($this->getRawPath() . '/' . /** @scrutinizer ignore-type */ $raw_ignore_add);
Loading history...
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
Bug introduced by
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 ignore-type  annotation

235
		$ignore_remove = realpath($this->getRawPath() . '/' . /** @scrutinizer ignore-type */ $raw_ignore_remove);
Loading history...
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
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
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
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
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