Failed Conditions
Push — master ( fe778b...ae8928 )
by Alexander
02:48
created

AggregateCommand::prepareSubCommandArguments()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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