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