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