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
![]() |
|||
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
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 ![]() |
|||
272 | throw new CommandException('No changelists detected.'); |
||
273 | } |
||
274 | |||
275 | return $this->io->choose( |
||
0 ignored issues
–
show
|
|||
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 |