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