Completed
Push — master ( 8fef4f...6746af )
by Alexander
03:54
created

AggregateCommand::getSubCommandNames()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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