Completed
Push — master ( 4288f6...92f818 )
by Alexander
28s queued 15s
created

CommitCommand::deploy()   A

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

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