Completed
Push — master ( 77fe96...8c07b1 )
by Alexander
02:27
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

Importance

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