Completed
Push — master ( 79b03c...d0c016 )
by Alexander
02:29
created

CommitCommand::getChangelist()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 19
ccs 0
cts 11
cp 0
rs 9.4285
cc 3
eloc 11
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\ConsoleKit\Exception\CommandException;
15
use ConsoleHelpers\SVNBuddy\InteractiveEditor;
16
use ConsoleHelpers\SVNBuddy\Repository\Parser\RevisionListParser;
17
use Symfony\Component\Console\Input\InputArgument;
18
use Symfony\Component\Console\Input\InputInterface;
19
use Symfony\Component\Console\Input\InputOption;
20
use Symfony\Component\Console\Output\OutputInterface;
21
22
class CommitCommand extends AbstractCommand
23
{
24
25
	const STOP_LINE = '--This line, and those below, will be ignored--';
26
27
	/**
28
	 * Revision list parser.
29
	 *
30
	 * @var RevisionListParser
31
	 */
32
	private $_revisionListParser;
33
34
	/**
35
	 * Editor.
36
	 *
37
	 * @var InteractiveEditor
38
	 */
39
	private $_editor;
40
41
	/**
42
	 * {@inheritdoc}
43
	 */
44
	protected function configure()
45
	{
46
		$this
47
			->setName('commit')
48
			->setDescription(
49
				'Send changes from your working copy to the repository'
50
			)
51
			->setAliases(array('ci'))
52
			->addArgument(
53
				'path',
54
				InputArgument::OPTIONAL,
55
				'Working copy path',
56
				'.'
57
			)
58
			->addOption(
59
				'cl',
60
				null,
61
				InputOption::VALUE_NONE,
62
				'Operate only on members of selected changelist'
63
			);
64
65
		parent::configure();
66
	}
67
68
	/**
69
	 * Prepare dependencies.
70
	 *
71
	 * @return void
72
	 */
73
	protected function prepareDependencies()
74
	{
75
		parent::prepareDependencies();
76
77
		$container = $this->getContainer();
78
79
		$this->_revisionListParser = $container['revision_list_parser'];
80
		$this->_editor = $container['editor'];
81
	}
82
83
	/**
84
	 * {@inheritdoc}
85
	 *
86
	 * @throws CommandException When conflicts are detected.
87
	 * @throws CommandException Working copy has no changes.
88
	 * @throws CommandException User decides not to perform a commit.
89
	 */
90
	protected function execute(InputInterface $input, OutputInterface $output)
91
	{
92
		$wc_path = $this->getWorkingCopyPath();
93
		$conflicts = $this->repositoryConnector->getWorkingCopyConflicts($wc_path);
94
95
		if ( $conflicts ) {
96
			throw new CommandException('Conflicts detected. Please resolve them before committing.');
97
		}
98
99
		$changelist = $this->getChangelist($wc_path);
100
		$compact_working_copy_status = $this->repositoryConnector->getCompactWorkingCopyStatus($wc_path, $changelist);
101
102
		if ( !$compact_working_copy_status ) {
103
			throw new CommandException('Nothing to commit.');
104
		}
105
106
		$commit_message = $this->buildCommitMessage($wc_path);
107
		$commit_message .= PHP_EOL . PHP_EOL . self::STOP_LINE . PHP_EOL . PHP_EOL . $compact_working_copy_status;
108
109
		$edited_commit_message = $this->_editor
110
			->setDocumentName('commit_message')
111
			->setContent($commit_message)
112
			->launch();
113
114
		$stop_line_pos = strpos($edited_commit_message, self::STOP_LINE);
115
116
		if ( $stop_line_pos !== false ) {
117
			$edited_commit_message = trim(substr($edited_commit_message, 0, $stop_line_pos));
118
		}
119
120
		$this->io->writeln(array('<fg=white;options=bold>Commit message:</>', $edited_commit_message, ''));
121
122
		if ( !$this->io->askConfirmation('Run "svn commit"', false) ) {
123
			throw new CommandException('Commit aborted by user.');
124
		}
125
126
		$tmp_file = tempnam(sys_get_temp_dir(), 'commit_message_');
127
		file_put_contents($tmp_file, $edited_commit_message);
128
129
		$arguments = array(
130
			'-F {' . $tmp_file . '}',
131
		);
132
133
		if ( strlen($changelist) ) {
134
			$arguments[] = '--depth empty';
135
136
			// Relative path used to make command line shorter.
137
			foreach ( array_keys($this->repositoryConnector->getWorkingCopyStatus($wc_path, $changelist)) as $path ) {
138
				$arguments[] = '{' . $path . '}';
139
			}
140
		}
141
		else {
142
			$arguments[] = '{' . $wc_path . '}';
143
		}
144
145
		$this->repositoryConnector->getCommand('commit', implode(' ', $arguments))->runLive();
146
		$this->setSetting(MergeCommand::SETTING_MERGE_RECENT_CONFLICTS, null, 'merge');
147
		unlink($tmp_file);
148
149
		$this->io->writeln('<info>Done</info>');
150
	}
151
152
	/**
153
	 * Returns user selected changelist.
154
	 *
155
	 * @param string $wc_path Working copy path.
156
	 *
157
	 * @return string|null
158
	 * @throws CommandException When no changelists found.
159
	 */
160
	protected function getChangelist($wc_path)
161
	{
162
		if ( !$this->io->getOption('cl') ) {
163
			return null;
164
		}
165
166
		$changelists = $this->repositoryConnector->getWorkingCopyChangelists($wc_path);
167
168
		if ( !$changelists ) {
169
			throw new CommandException('No changelists detected.');
170
		}
171
172
		return $this->io->choose(
173
			'Pick changelist by number [0]:',
174
			$changelists,
175
			0,
176
			'Changelist "%s" is invalid.'
177
		);
178
	}
179
180
	/**
181
	 * Builds a commit message.
182
	 *
183
	 * @param string $wc_path Working copy path.
184
	 *
185
	 * @return string
186
	 */
187
	protected function buildCommitMessage($wc_path)
188
	{
189
		/*
190
		 * 3. if it's In-Portal project, then:
191
		 * - create commit message that:
192
		 * -- Merge of "{from_path}@{from_rev}" to "{to_path}@{to_rev}".
193
		 * -- Merge of "in-portal/branches/5.2.x@16189" to "in-portal/branches/5.3.x@16188".
194
		 * - {from_path} to be determined from list of merged revisions
195
		 * - {from_rev} - last changed of {from_path} by looking in repo
196
		 * - {to_path} to be determined from working copy
197
		 * - {to_rev} - last changed of {to_path} by looking in repo
198
		 * 4. open interactive editor with auto-generated message
199
		 */
200
201
		$merged_revisions = $this->getFreshMergedRevisions($wc_path);
202
203
		if ( !$merged_revisions ) {
204
			return '';
205
		}
206
207
		$commit_message = '';
208
		$wc_url = $this->repositoryConnector->getWorkingCopyUrl($wc_path);
209
		$repository_url = $this->repositoryConnector->getRootUrl($wc_url);
210
211
		foreach ( $merged_revisions as $path => $revisions ) {
212
			$merged_messages = array();
213
			$revision_log = $this->getRevisionLog($repository_url . $path);
214
			$commit_message .= $this->getCommitMessageHeading($wc_url, $path) . PHP_EOL;
215
216
			$revisions_data = $revision_log->getRevisionsData('summary', $revisions);
217
218
			foreach ( $revisions as $revision ) {
219
				$merged_messages[] = ' * r' . $revision . ': ' . $revisions_data[$revision]['msg'];
220
			}
221
222
			$merged_messages = array_unique(array_map('trim', $merged_messages));
223
			$commit_message .= implode(PHP_EOL, $merged_messages) . PHP_EOL;
224
		}
225
226
		$commit_message .= $this->getCommitMessageConflicts();
227
228
		return rtrim($commit_message);
229
	}
230
231
	/**
232
	 * Builds commit message heading.
233
	 *
234
	 * @param string $wc_url Working copy url.
235
	 * @param string $path   Source path for merge operation.
236
	 *
237
	 * @return string
238
	 */
239
	protected function getCommitMessageHeading($wc_url, $path)
240
	{
241
		return 'Merging from ' . ucfirst(basename($path)) . ' to ' . ucfirst(basename($wc_url));
242
	}
243
244
	/**
245
	 * Returns recent merge conflicts.
246
	 *
247
	 * @return string
248
	 */
249
	protected function getCommitMessageConflicts()
250
	{
251
		$recent_conflicts = $this->getSetting(MergeCommand::SETTING_MERGE_RECENT_CONFLICTS, 'merge');
252
253
		if ( !$recent_conflicts ) {
254
			return '';
255
		}
256
257
		$ret = PHP_EOL . 'Conflicts:' . PHP_EOL;
258
259
		foreach ( $recent_conflicts as $conflict_path ) {
260
			$ret .= ' * ' . $conflict_path . PHP_EOL;
261
		}
262
263
		return $ret;
264
	}
265
266
	/**
267
	 * Returns list of just merged revisions.
268
	 *
269
	 * @param string $wc_path Merge target: working copy path.
270
	 *
271
	 * @return array
272
	 */
273
	protected function getFreshMergedRevisions($wc_path)
274
	{
275
		$final_paths = array();
276
		$old_paths = $this->getMergedRevisions($wc_path, 'BASE');
277
		$new_paths = $this->getMergedRevisions($wc_path);
278
279
		if ( $old_paths === $new_paths ) {
280
			return array();
281
		}
282
283
		foreach ( $new_paths as $new_path => $new_merged_revisions ) {
284
			if ( !isset($old_paths[$new_path]) ) {
285
				// Merge from new path.
286
				$final_paths[$new_path] = $this->_revisionListParser->expandRanges(
287
					explode(',', $new_merged_revisions)
288
				);
289
			}
290
			elseif ( $new_merged_revisions != $old_paths[$new_path] ) {
291
				// Merge on existing path.
292
				$new_merged_revisions_parsed = $this->_revisionListParser->expandRanges(
293
					explode(',', $new_merged_revisions)
294
				);
295
				$old_merged_revisions_parsed = $this->_revisionListParser->expandRanges(
296
					explode(',', $old_paths[$new_path])
297
				);
298
				$final_paths[$new_path] = array_values(
299
					array_diff($new_merged_revisions_parsed, $old_merged_revisions_parsed)
300
				);
301
			}
302
		}
303
304
		return $final_paths;
305
	}
306
307
	/**
308
	 * Returns list of merged revisions per path.
309
	 *
310
	 * @param string  $wc_path  Merge target: working copy path.
311
	 * @param integer $revision Revision.
312
	 *
313
	 * @return array
314
	 */
315
	protected function getMergedRevisions($wc_path, $revision = null)
316
	{
317
		$paths = array();
318
319
		$merge_info = $this->repositoryConnector->getProperty('svn:mergeinfo', $wc_path, $revision);
320
		$merge_info = array_filter(explode("\n", $merge_info));
321
322
		foreach ( $merge_info as $merge_info_line ) {
323
			list($path, $revisions) = explode(':', $merge_info_line, 2);
324
			$paths[$path] = $revisions;
325
		}
326
327
		return $paths;
328
	}
329
330
}
331