001 package org.apache.maven.scm.provider.hg;
002
003 /*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements. See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership. The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License. You may obtain a copy of the License at
011 *
012 * http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied. See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022 import org.apache.maven.scm.ScmException;
023 import org.apache.maven.scm.ScmFileSet;
024 import org.apache.maven.scm.ScmFileStatus;
025 import org.apache.maven.scm.ScmResult;
026 import org.apache.maven.scm.log.DefaultLog;
027 import org.apache.maven.scm.log.ScmLogger;
028 import org.apache.maven.scm.provider.hg.command.HgCommandConstants;
029 import org.apache.maven.scm.provider.hg.command.HgConsumer;
030 import org.apache.maven.scm.provider.hg.command.inventory.HgChangeSet;
031 import org.apache.maven.scm.provider.hg.command.inventory.HgOutgoingConsumer;
032 import org.codehaus.plexus.util.cli.CommandLineException;
033 import org.codehaus.plexus.util.cli.CommandLineUtils;
034 import org.codehaus.plexus.util.cli.Commandline;
035
036 import java.io.File;
037 import java.util.ArrayList;
038 import java.util.HashMap;
039 import java.util.List;
040 import java.util.Map;
041
042 /**
043 * Common code for executing hg commands.
044 *
045 * @author <a href="mailto:thurner.rupert@ymono.net">thurner rupert</a>
046 *
047 */
048 public final class HgUtils
049 {
050
051 private HgUtils()
052 {
053 }
054
055 /**
056 * Map between command and its valid exit codes
057 */
058 private static final Map<String, List<Integer>> EXIT_CODE_MAP = new HashMap<String, List<Integer>>();
059
060 /**
061 * Default exit codes for entries not in exitCodeMap
062 */
063 private static final List<Integer> DEFAULT_EXIT_CODES = new ArrayList<Integer>();
064
065 /** Setup exit codes*/
066 static
067 {
068 DEFAULT_EXIT_CODES.add( new Integer( 0 ) );
069
070 //Diff is different
071 List<Integer> diffExitCodes = new ArrayList<Integer>( 3 );
072 diffExitCodes.add( Integer.valueOf( 0 ) ); //No difference
073 diffExitCodes.add( Integer.valueOf( 1 ) ); //Conflicts in merge-like or changes in diff-like
074 diffExitCodes.add( Integer.valueOf( 2 ) ); //Unrepresentable diff changes
075 EXIT_CODE_MAP.put( HgCommandConstants.DIFF_CMD, diffExitCodes );
076 //Outgoing is different
077 List<Integer> outgoingExitCodes = new ArrayList<Integer>( 2 );
078 outgoingExitCodes.add( Integer.valueOf( 0 ) ); //There are changes
079 outgoingExitCodes.add( Integer.valueOf( 1 ) ); //No changes
080 EXIT_CODE_MAP.put( HgCommandConstants.OUTGOING_CMD, outgoingExitCodes );
081 }
082
083 public static ScmResult execute( HgConsumer consumer, ScmLogger logger, File workingDir, String[] cmdAndArgs )
084 throws ScmException
085 {
086 try
087 {
088 //Build commandline
089 Commandline cmd = buildCmd( workingDir, cmdAndArgs );
090 if ( logger.isInfoEnabled() )
091 {
092 logger.info( "EXECUTING: " + maskPassword( cmd ) );
093 }
094
095 //Execute command
096 int exitCode = executeCmd( consumer, cmd );
097
098 //Return result
099 List<Integer> exitCodes = DEFAULT_EXIT_CODES;
100 if ( EXIT_CODE_MAP.containsKey( cmdAndArgs[0] ) )
101 {
102 exitCodes = EXIT_CODE_MAP.get( cmdAndArgs[0] );
103 }
104 boolean success = exitCodes.contains( Integer.valueOf( exitCode ) );
105
106 //On failure (and not due to exceptions) - run diagnostics
107 String providerMsg = "Execution of hg command succeded";
108 if ( !success )
109 {
110 HgConfig config = new HgConfig( workingDir );
111 providerMsg =
112 "\nEXECUTION FAILED" + "\n Execution of cmd : " + cmdAndArgs[0] + " failed with exit code: "
113 + exitCode + "." + "\n Working directory was: " + "\n " + workingDir.getAbsolutePath()
114 + config.toString( workingDir ) + "\n";
115 if ( logger.isErrorEnabled() )
116 {
117 logger.error( providerMsg );
118 }
119 }
120
121 return new ScmResult( cmd.toString(), providerMsg, consumer.getStdErr(), success );
122 }
123 catch ( ScmException se )
124 {
125 String msg =
126 "EXECUTION FAILED" + "\n Execution failed before invoking the Hg command. Last exception:" + "\n "
127 + se.getMessage();
128
129 //Add nested cause if any
130 if ( se.getCause() != null )
131 {
132 msg += "\n Nested exception:" + "\n " + se.getCause().getMessage();
133 }
134
135 //log and return
136 if ( logger.isErrorEnabled() )
137 {
138 logger.error( msg );
139 }
140 throw se;
141 }
142 }
143
144 static Commandline buildCmd( File workingDir, String[] cmdAndArgs )
145 throws ScmException
146 {
147 Commandline cmd = new Commandline();
148 cmd.setExecutable( HgCommandConstants.EXEC );
149 cmd.addArguments( cmdAndArgs );
150 if ( workingDir != null )
151 {
152 cmd.setWorkingDirectory( workingDir.getAbsolutePath() );
153
154 if ( !workingDir.exists() )
155 {
156 boolean success = workingDir.mkdirs();
157 if ( !success )
158 {
159 String msg = "Working directory did not exist" + " and it couldn't be created: " + workingDir;
160 throw new ScmException( msg );
161 }
162 }
163 }
164 return cmd;
165 }
166
167 static int executeCmd( HgConsumer consumer, Commandline cmd )
168 throws ScmException
169 {
170 final int exitCode;
171 try
172 {
173 exitCode = CommandLineUtils.executeCommandLine( cmd, consumer, consumer );
174 }
175 catch ( CommandLineException ex )
176 {
177 throw new ScmException( "Command could not be executed: " + cmd, ex );
178 }
179 return exitCode;
180 }
181
182 public static ScmResult execute( File workingDir, String[] cmdAndArgs )
183 throws ScmException
184 {
185 ScmLogger logger = new DefaultLog();
186 return execute( new HgConsumer( logger ), logger, workingDir, cmdAndArgs );
187 }
188
189 public static String[] expandCommandLine( String[] cmdAndArgs, ScmFileSet additionalFiles )
190 {
191 List<File> filesList = additionalFiles.getFileList();
192 String[] cmd = new String[filesList.size() + cmdAndArgs.length];
193
194 // Copy command into array
195 System.arraycopy( cmdAndArgs, 0, cmd, 0, cmdAndArgs.length );
196
197 // Add files as additional parameter into the array
198 int i = 0;
199 for ( File scmFile : filesList )
200 {
201 String file = scmFile.getPath().replace( '\\', File.separatorChar );
202 cmd[i + cmdAndArgs.length] = file;
203 i++;
204 }
205
206 return cmd;
207 }
208
209 public static int getCurrentRevisionNumber( ScmLogger logger, File workingDir )
210 throws ScmException
211 {
212
213 String[] revCmd = new String[]{ HgCommandConstants.REVNO_CMD };
214 HgRevNoConsumer consumer = new HgRevNoConsumer( logger );
215 HgUtils.execute( consumer, logger, workingDir, revCmd );
216
217 return consumer.getCurrentRevisionNumber();
218 }
219
220 public static String getCurrentBranchName( ScmLogger logger, File workingDir )
221 throws ScmException
222 {
223 String[] branchnameCmd = new String[]{ HgCommandConstants.BRANCH_NAME_CMD };
224 HgBranchnameConsumer consumer = new HgBranchnameConsumer( logger );
225 HgUtils.execute( consumer, logger, workingDir, branchnameCmd );
226 return consumer.getBranchName();
227 }
228
229 /**
230 * Get current (working) revision.
231 * <p/>
232 * Resolve revision to the last integer found in the command output.
233 */
234 private static class HgRevNoConsumer
235 extends HgConsumer
236 {
237
238 private int revNo;
239
240 HgRevNoConsumer( ScmLogger logger )
241 {
242 super( logger );
243 }
244
245 public void doConsume( ScmFileStatus status, String line )
246 {
247 try
248 {
249 revNo = Integer.valueOf( line ).intValue();
250 }
251 catch ( NumberFormatException e )
252 {
253 // ignore
254 }
255 }
256
257 int getCurrentRevisionNumber()
258 {
259 return revNo;
260 }
261 }
262
263 /**
264 * Get current (working) branch name
265 */
266 private static class HgBranchnameConsumer
267 extends HgConsumer
268 {
269
270 private String branchName;
271
272 HgBranchnameConsumer( ScmLogger logger )
273 {
274 super( logger );
275 }
276
277 public void doConsume( ScmFileStatus status, String trimmedLine )
278 {
279 branchName = String.valueOf( trimmedLine );
280 }
281
282 String getBranchName()
283 {
284 return branchName;
285 }
286 }
287
288
289 /**
290 * Check if there are outgoing changes on a different branch. If so, Mercurial default behaviour
291 * is to block the push and warn using a 'push creates new remote branch !' message.
292 * We also warn, and return true if a different outgoing branch was found
293 * <p/>
294 * Method users should not stop the push on a negative return, instead, they should hg push -r(branch being released)
295 *
296 * @param logger the logger
297 * @param workingDir the working dir
298 * @param workingbranchName the working branch name
299 * @return true if a different outgoing branch was found
300 * @throws ScmException on outgoing command error
301 */
302 public static boolean differentOutgoingBranchFound( ScmLogger logger, File workingDir, String workingbranchName )
303 throws ScmException
304 {
305 String[] outCmd = new String[]{ HgCommandConstants.OUTGOING_CMD };
306 HgOutgoingConsumer outConsumer = new HgOutgoingConsumer( logger );
307 ScmResult outResult = HgUtils.execute( outConsumer, logger, workingDir, outCmd );
308 List<HgChangeSet> changes = outConsumer.getChanges();
309 if ( outResult.isSuccess() )
310 {
311 for ( HgChangeSet set : changes )
312 {
313 if ( set.getBranch() != null )
314 {
315 logger.warn( "A different branch than " + workingbranchName
316 + " was found in outgoing changes, branch name was " + set.getBranch()
317 + ". Only local branch named " + workingbranchName + " will be pushed." );
318 return true;
319 }
320 }
321 }
322 return false;
323 }
324
325 public static String maskPassword( Commandline cl )
326 {
327 String clString = cl.toString();
328
329 int pos = clString.indexOf( '@' );
330
331 if ( pos > 0 )
332 {
333 clString = clString.replaceAll( ":\\w+@", ":*****@" );
334 }
335
336 return clString;
337 }
338 }