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