CommitCommand::getMergeTemplate()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 9
ccs 0
cts 5
cp 0
rs 10
cc 2
nc 2
nop 0
crap 6
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\ChoiceConfigSetting;
17
use ConsoleHelpers\SVNBuddy\InteractiveEditor;
18
use ConsoleHelpers\SVNBuddy\Repository\CommitMessage\AbstractMergeTemplate;
19
use ConsoleHelpers\SVNBuddy\Repository\CommitMessage\CommitMessageBuilder;
20
use ConsoleHelpers\SVNBuddy\Repository\CommitMessage\MergeTemplateFactory;
21
use ConsoleHelpers\SVNBuddy\Repository\WorkingCopyConflictTracker;
22
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
23
use Symfony\Component\Console\Input\InputArgument;
24
use Symfony\Component\Console\Input\InputInterface;
25
use Symfony\Component\Console\Input\InputOption;
26
use Symfony\Component\Console\Output\OutputInterface;
27
28
class CommitCommand extends AbstractCommand implements IConfigAwareCommand
29
{
30
31
	const SETTING_COMMIT_MERGE_TEMPLATE = 'commit.merge-template';
32
33
	const SETTING_COMMIT_AUTO_DEPLOY = 'commit.auto-deploy';
34
35
	const STOP_LINE = '--This line, and those below, will be ignored--';
36
37
	/**
38
	 * Editor.
39
	 *
40
	 * @var InteractiveEditor
41
	 */
42
	private $_editor;
43
44
	/**
45
	 * Commit message builder.
46
	 *
47
	 * @var CommitMessageBuilder
48
	 */
49
	private $_commitMessageBuilder;
50
51
	/**
52
	 * Merge template factory.
53
	 *
54
	 * @var MergeTemplateFactory
55
	 */
56
	private $_mergeTemplateFactory;
57
58
	/**
59
	 * Working copy conflict tracker.
60
	 *
61
	 * @var WorkingCopyConflictTracker
62
	 */
63
	private $_workingCopyConflictTracker;
64
65
	/**
66
	 * {@inheritdoc}
67
	 */
68
	protected function configure()
69
	{
70
		$this
71
			->setName('commit')
72
			->setDescription(
73
				'Send changes from your working copy to the repository'
74
			)
75
			->setAliases(array('ci'))
76
			->addArgument(
77
				'path',
78
				InputArgument::OPTIONAL,
79
				'Working copy path',
80
				'.'
81
			)
82
			->addOption(
83
				'cl',
84
				null,
85
				InputOption::VALUE_NONE,
86
				'Operate only on members of selected changelist'
87
			)
88
			->addOption(
89
				'merge-template',
90
				null,
91
				InputOption::VALUE_REQUIRED,
92
				'Use alternative merge template for this commit'
93
			)
94
			->addOption(
95
				'auto-deploy',
96
				null,
97
				InputOption::VALUE_REQUIRED,
98
				'Automatically perform remote deployment on successful commit, e.g. <comment>yes</comment> or <comment>no</comment>'
99
			);
100
101
		parent::configure();
102
	}
103
104
	/**
105
	 * Return possible values for the named option
106
	 *
107
	 * @param string            $optionName Option name.
108
	 * @param CompletionContext $context    Completion context.
109
	 *
110
	 * @return array
111
	 */
112
	public function completeOptionValues($optionName, CompletionContext $context)
113
	{
114
		$ret = parent::completeOptionValues($optionName, $context);
115
116
		if ( $optionName === 'merge-template' ) {
117
			return $this->getMergeTemplateNames();
118
		}
119
120
		if ( $optionName === 'auto-deploy' ) {
121
			return array('yes', 'no');
122
		}
123
124
		return $ret;
125
	}
126
127
	/**
128
	 * Prepare dependencies.
129
	 *
130
	 * @return void
131
	 */
132
	protected function prepareDependencies()
133
	{
134
		parent::prepareDependencies();
135
136
		$container = $this->getContainer();
137
138
		$this->_editor = $container['editor'];
139
		$this->_commitMessageBuilder = $container['commit_message_builder'];
140
		$this->_mergeTemplateFactory = $container['merge_template_factory'];
141
		$this->_workingCopyConflictTracker = $container['working_copy_conflict_tracker'];
142
	}
143
144
	/**
145
	 * {@inheritdoc}
146
	 *
147
	 * @throws CommandException When conflicts are detected.
148
	 * @throws CommandException Working copy has no changes.
149
	 * @throws CommandException User decides not to perform a commit.
150
	 */
151
	protected function execute(InputInterface $input, OutputInterface $output)
152
	{
153
		$wc_path = $this->getWorkingCopyPath();
154
		$conflicts = $this->_workingCopyConflictTracker->getNewConflicts($wc_path);
155
156
		if ( $conflicts ) {
157
			throw new CommandException('Conflicts detected. Please resolve them before committing.');
158
		}
159
160
		$changelist = $this->getChangelist($wc_path);
161
		$compact_working_copy_status = $this->repositoryConnector->getCompactWorkingCopyStatus($wc_path, $changelist);
162
163
		if ( !$compact_working_copy_status ) {
164
			// Deploy instead of failing.
165
			if ( $this->deploy() ) {
166
				return;
167
			}
168
169
			throw new CommandException('Nothing to commit.');
170
		}
171
172
		$commit_message = $this->_commitMessageBuilder->build($wc_path, $this->getMergeTemplate(), $changelist);
173
		$commit_message .= PHP_EOL . PHP_EOL . self::STOP_LINE . PHP_EOL . PHP_EOL . $compact_working_copy_status;
174
175
		$edited_commit_message = $this->_editor
176
			->setDocumentName('commit_message')
177
			->setContent($commit_message)
178
			->launch();
179
180
		$stop_line_pos = strpos($edited_commit_message, self::STOP_LINE);
181
182
		if ( $stop_line_pos !== false ) {
183
			$edited_commit_message = trim(substr($edited_commit_message, 0, $stop_line_pos));
184
		}
185
186
		$this->io->writeln(array('<fg=white;options=bold>Commit message:</>', $edited_commit_message, ''));
187
188
		if ( !$this->io->askConfirmation('Run "svn commit"', false) ) {
189
			throw new CommandException('Commit aborted by user.');
190
		}
191
192
		$tmp_file = tempnam(sys_get_temp_dir(), 'commit_message_');
193
		file_put_contents($tmp_file, $edited_commit_message);
194
195
		$arguments = array('-F', $tmp_file);
196
197
		if ( strlen($changelist) ) {
0 ignored issues
show
Bug introduced by
It seems like $changelist can also be of type null; however, parameter $string of strlen() 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

197
		if ( strlen(/** @scrutinizer ignore-type */ $changelist) ) {
Loading history...
198
			$arguments[] = '--depth';
199
			$arguments[] = 'empty';
200
201
			// Relative path used to make command line shorter.
202
			foreach ( array_keys($this->repositoryConnector->getWorkingCopyStatus($wc_path, $changelist)) as $path ) {
203
				$arguments[] = $path;
204
			}
205
		}
206
		else {
207
			$arguments[] = $wc_path;
208
		}
209
210
		$this->repositoryConnector->getCommand('commit', $arguments)->runLive(array(
211
			'/(Committed revision [\d]+\.)/' => '<fg=white;options=bold>$1</>',
212
		));
213
		$this->_workingCopyConflictTracker->erase($wc_path);
214
		unlink($tmp_file);
215
216
		// Make committed revision instantly available for merging.
217
		$this->getRevisionLog($this->getWorkingCopyUrl())->setForceRefreshFlag(true);
218
219
		$this->io->writeln('<info>Done</info>');
220
221
		$this->deploy();
222
	}
223
224
	/**
225
	 * Performs a deploy.
226
	 *
227
	 * @return boolean
228
	 */
229
	protected function deploy()
230
	{
231
		$auto_deploy = $this->io->getOption('auto-deploy');
232
233
		if ( $auto_deploy !== null ) {
234
			$auto_deploy = $auto_deploy === 'yes';
235
		}
236
		else {
237
			$auto_deploy = (boolean)$this->getSetting(self::SETTING_COMMIT_AUTO_DEPLOY);
238
		}
239
240
		if ( $auto_deploy ) {
241
			$this->runOtherCommand(
242
				'deploy',
243
				array(
244
					'path' => $this->io->getArgument('path'),
245
					'--remote' => true,
246
				)
247
			);
248
249
			return true;
250
		}
251
252
		return false;
253
	}
254
255
	/**
256
	 * Returns user selected changelist.
257
	 *
258
	 * @param string $wc_path Working copy path.
259
	 *
260
	 * @return string|null
261
	 * @throws CommandException When no changelists found.
262
	 */
263
	protected function getChangelist($wc_path)
264
	{
265
		if ( !$this->io->getOption('cl') ) {
266
			return null;
267
		}
268
269
		$changelists = $this->repositoryConnector->getWorkingCopyChangelists($wc_path);
270
271
		if ( !$changelists ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $changelists 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...
272
			throw new CommandException('No changelists detected.');
273
		}
274
275
		return $this->io->choose(
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->io->choose...list "%s" is invalid.') also could return the type boolean|string[] which is incompatible with the documented return type null|string.
Loading history...
276
			'Pick changelist by number [0]:',
277
			$changelists,
278
			0,
279
			'Changelist "%s" is invalid.'
280
		);
281
	}
282
283
	/**
284
	 * Returns merge template to use.
285
	 *
286
	 * @return AbstractMergeTemplate
287
	 */
288
	protected function getMergeTemplate()
289
	{
290
		$merge_template_name = $this->io->getOption('merge-template');
291
292
		if ( !isset($merge_template_name) ) {
293
			$merge_template_name = $this->getSetting(self::SETTING_COMMIT_MERGE_TEMPLATE);
294
		}
295
296
		return $this->_mergeTemplateFactory->get($merge_template_name);
297
	}
298
299
	/**
300
	 * Returns merge template names.
301
	 *
302
	 * @return array
303
	 */
304
	protected function getMergeTemplateNames()
305
	{
306
		if ( isset($this->_mergeTemplateFactory) ) {
307
			return $this->_mergeTemplateFactory->getNames();
308
		}
309
310
		// When used from "getConfigSettings" method.
311
		$container = $this->getContainer();
312
313
		return $container['merge_template_factory']->getNames();
314
	}
315
316
	/**
317
	 * Returns list of config settings.
318
	 *
319
	 * @return AbstractConfigSetting[]
320
	 */
321
	public function getConfigSettings()
322
	{
323
		$merge_template_names = array();
324
325
		foreach ( $this->getMergeTemplateNames() as $merge_template_name ) {
326
			$merge_template_names[$merge_template_name] = str_replace('_', ' ', ucfirst($merge_template_name));
327
		}
328
329
		return array(
330
			new ChoiceConfigSetting(
331
				self::SETTING_COMMIT_MERGE_TEMPLATE,
332
				$merge_template_names,
333
				reset($merge_template_names)
334
			),
335
			new ChoiceConfigSetting(
336
				self::SETTING_COMMIT_AUTO_DEPLOY,
337
				array(1 => 'Yes', 0 => 'No'),
338
				1
339
			),
340
		);
341
	}
342
343
}
344