Completed
Push — master ( 165a98...7e05e5 )
by Alexander
02:32
created

CommitCommand::getFreshMergedRevisions()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 33
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30
Metric Value
dl 0
loc 33
ccs 0
cts 25
cp 0
rs 8.439
cc 5
eloc 18
nc 5
nop 1
crap 30
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\Output\OutputInterface;
20
21
class CommitCommand extends AbstractCommand
22
{
23
24
	const STOP_LINE = '--This line, and those below, will be ignored--';
25
26
	/**
27
	 * Revision list parser.
28
	 *
29
	 * @var RevisionListParser
30
	 */
31
	private $_revisionListParser;
32
33
	/**
34
	 * Editor.
35
	 *
36
	 * @var InteractiveEditor
37
	 */
38
	private $_editor;
39
40
	/**
41
	 * {@inheritdoc}
0 ignored issues
show
introduced by
Doc comment short description must start with a capital letter
Loading history...
42
	 */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
43
	protected function configure()
44
	{
45
		$description = <<<TEXT
46
TODO
47
TEXT;
48
49
		$this
50
			->setName('commit')
51
			->setDescription(
52
				'Sends changes to repository'
53
			)
54
			->setHelp($description)
55
			->setAliases(array('ci'))
56
			->addArgument(
57
				'path',
58
				InputArgument::OPTIONAL,
59
				'Working copy path',
60
				'.'
61
			);
62
63
		parent::configure();
64
	}
65
66
	/**
67
	 * Prepare dependencies.
68
	 *
69
	 * @return void
70
	 */
71
	protected function prepareDependencies()
72
	{
73
		parent::prepareDependencies();
74
75
		$container = $this->getContainer();
76
77
		$this->_revisionListParser = $container['revision_list_parser'];
78
		$this->_editor = $container['editor'];
79
	}
80
81
	/**
0 ignored issues
show
introduced by
Doc comment for parameter "$input" missing
Loading history...
introduced by
Doc comment for parameter "$output" missing
Loading history...
82
	 * {@inheritdoc}
0 ignored issues
show
introduced by
Doc comment short description must start with a capital letter
Loading history...
83
	 */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
Coding Style Documentation introduced by
Missing @throws tag in function comment
Loading history...
84
	protected function execute(InputInterface $input, OutputInterface $output)
85
	{
86
		$wc_path = $this->getWorkingCopyPath();
87
		$conflicts = $this->repositoryConnector->getWorkingCopyConflicts($wc_path);
88
89
		if ( $conflicts ) {
90
			throw new CommandException('Conflicts detected. Please resolve them before committing.');
91
		}
92
93
		$working_copy_status = $this->repositoryConnector->getCompactWorkingCopyStatus($wc_path, false);
94
95
		if ( !$working_copy_status ) {
96
			throw new CommandException('Nothing to commit.');
97
		}
98
99
		$commit_message = $this->buildCommitMessage($wc_path);
100
		$commit_message .= PHP_EOL . PHP_EOL . self::STOP_LINE . PHP_EOL . PHP_EOL . $working_copy_status;
101
102
		$edited_commit_message = $this->_editor
103
			->setDocumentName('commit_message')
104
			->setContent($commit_message)
105
			->launch();
106
107
		$stop_line_pos = strpos($edited_commit_message, self::STOP_LINE);
108
109
		if ( $stop_line_pos !== false ) {
110
			$edited_commit_message = trim(substr($edited_commit_message, 0, $stop_line_pos));
111
		}
112
113
		$this->io->writeln(array('<fg=white;options=bold>Commit message:</>', $edited_commit_message, ''));
114
115
		if ( !$this->io->askConfirmation('Run "svn commit"', false) ) {
116
			throw new CommandException('Commit aborted by user.');
117
		}
118
119
		$tmp_file = tempnam(sys_get_temp_dir(), 'commit_message_');
120
		file_put_contents($tmp_file, $edited_commit_message);
121
122
		$this->repositoryConnector->getCommand('commit', '{' . $wc_path . '} -F {' . $tmp_file . '}')->runLive();
123
		$this->setSetting(MergeCommand::SETTING_MERGE_RECENT_CONFLICTS, null, 'merge');
124
		unlink($tmp_file);
125
126
		$this->io->writeln('<info>Done</info>');
127
	}
128
129
	/**
130
	 * Builds a commit message.
131
	 *
132
	 * @param string $wc_path Working copy path.
133
	 *
134
	 * @return string
135
	 */
136
	protected function buildCommitMessage($wc_path)
137
	{
138
		/*
139
		 * 3. if it's In-Portal project, then:
140
		 * - create commit message that:
141
		 * -- Merge of "{from_path}@{from_rev}" to "{to_path}@{to_rev}".
142
		 * -- Merge of "in-portal/branches/5.2.x@16189" to "in-portal/branches/5.3.x@16188".
143
		 * - {from_path} to be determined from list of merged revisions
144
		 * - {from_rev} - last changed of {from_path} by looking in repo
145
		 * - {to_path} to be determined from working copy
146
		 * - {to_rev} - last changed of {to_path} by looking in repo
147
		 * 4. open interactive editor with auto-generated message
148
		 */
149
150
		$merged_revisions = $this->getFreshMergedRevisions($wc_path);
151
152
		if ( !$merged_revisions ) {
153
			return '';
154
		}
155
156
		$commit_message = '';
157
		$wc_url = $this->repositoryConnector->getWorkingCopyUrl($wc_path);
158
		$repository_url = $this->removePathFromURL($wc_url);
159
160
		foreach ( $merged_revisions as $path => $revisions ) {
161
			$merged_messages = array();
162
			$revision_log = $this->getRevisionLog($repository_url . $path);
163
			$commit_message .= $this->getCommitMessageHeading($wc_url, $path) . PHP_EOL;
164
165
			foreach ( $revisions as $revision ) {
166
				$revision_data = $revision_log->getRevisionData('summary', $revision);
167
				$merged_messages[] = ' * r' . $revision . ': ' . $revision_data['msg'];
168
			}
169
170
			$merged_messages = array_unique(array_map('trim', $merged_messages));
171
			$commit_message .= implode(PHP_EOL, $merged_messages) . PHP_EOL;
172
		}
173
174
		$commit_message .= $this->getCommitMessageConflicts();
175
176
		return rtrim($commit_message);
177
	}
178
179
	/**
180
	 * Builds commit message heading.
181
	 *
182
	 * @param string $wc_url Working copy url.
183
	 * @param string $path   Source path for merge operation.
184
	 *
185
	 * @return string
186
	 */
187
	protected function getCommitMessageHeading($wc_url, $path)
188
	{
189
		return 'Merging from ' . ucfirst(basename($path)) . ' to ' . ucfirst(basename($wc_url));
190
	}
191
192
	/**
193
	 * Returns recent merge conflicts.
194
	 *
195
	 * @return string
196
	 */
197
	protected function getCommitMessageConflicts()
198
	{
199
		$recent_conflicts = $this->getSetting(MergeCommand::SETTING_MERGE_RECENT_CONFLICTS, 'merge');
200
201
		if ( !$recent_conflicts ) {
202
			return '';
203
		}
204
205
		$ret = PHP_EOL . 'Conflicts:' . PHP_EOL;
206
207
		foreach ( $recent_conflicts as $conflict_path ) {
208
			$ret .= ' * ' . $conflict_path . PHP_EOL;
209
		}
210
211
		return $ret;
212
	}
213
214
	/**
215
	 * Removes path component from URL.
216
	 *
217
	 * @param string $url URL.
218
	 *
219
	 * @return string
220
	 */
221
	protected function removePathFromURL($url)
222
	{
223
		$path = parse_url($url, PHP_URL_PATH);
224
225
		return preg_replace('#' . preg_quote($path, '#') . '$#', '', $url, 1);
226
	}
227
228
	/**
229
	 * Returns list of just merged revisions.
230
	 *
231
	 * @param string $wc_path Merge target: working copy path.
232
	 *
233
	 * @return array
234
	 */
235
	protected function getFreshMergedRevisions($wc_path)
236
	{
237
		$final_paths = array();
238
		$old_paths = $this->getMergedRevisions($wc_path, 'BASE');
239
		$new_paths = $this->getMergedRevisions($wc_path);
240
241
		if ( $old_paths === $new_paths ) {
242
			return array();
243
		}
244
245
		foreach ( $new_paths as $new_path => $new_merged_revisions ) {
246
			if ( !isset($old_paths[$new_path]) ) {
247
				// Merge from new path.
248
				$final_paths[$new_path] = $this->_revisionListParser->expandRanges(
249
					explode(',', $new_merged_revisions)
250
				);
251
			}
252
			elseif ( $new_merged_revisions != $old_paths[$new_path] ) {
253
				// Merge on existing path.
254
				$new_merged_revisions_parsed = $this->_revisionListParser->expandRanges(
255
					explode(',', $new_merged_revisions)
256
				);
257
				$old_merged_revisions_parsed = $this->_revisionListParser->expandRanges(
258
					explode(',', $old_paths[$new_path])
259
				);
260
				$final_paths[$new_path] = array_values(
261
					array_diff($new_merged_revisions_parsed, $old_merged_revisions_parsed)
262
				);
263
			}
264
		}
265
266
		return $final_paths;
267
	}
268
269
	/**
270
	 * Returns list of merged revisions per path.
271
	 *
272
	 * @param string  $wc_path  Merge target: working copy path.
273
	 * @param integer $revision Revision.
274
	 *
275
	 * @return array
276
	 */
277
	protected function getMergedRevisions($wc_path, $revision = null)
278
	{
279
		$paths = array();
280
281
		$merge_info = $this->repositoryConnector->getProperty('svn:mergeinfo', $wc_path, $revision);
282
		$merge_info = array_filter(explode("\n", $merge_info));
283
284
		foreach ( $merge_info as $merge_info_line ) {
285
			list($path, $revisions) = explode(':', $merge_info_line, 2);
286
			$paths[$path] = $revisions;
287
		}
288
289
		return $paths;
290
	}
291
292
}
293