View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.scm.provider.hg;
20  
21  import java.io.File;
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  
27  import org.apache.maven.scm.ScmException;
28  import org.apache.maven.scm.ScmFileSet;
29  import org.apache.maven.scm.ScmFileStatus;
30  import org.apache.maven.scm.ScmResult;
31  import org.apache.maven.scm.provider.hg.command.HgCommandConstants;
32  import org.apache.maven.scm.provider.hg.command.HgConsumer;
33  import org.apache.maven.scm.provider.hg.command.inventory.HgChangeSet;
34  import org.apache.maven.scm.provider.hg.command.inventory.HgOutgoingConsumer;
35  import org.codehaus.plexus.util.cli.CommandLineException;
36  import org.codehaus.plexus.util.cli.CommandLineUtils;
37  import org.codehaus.plexus.util.cli.Commandline;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  /**
42   * Common code for executing hg commands.
43   *
44   * @author <a href="mailto:thurner.rupert@ymono.net">thurner rupert</a>
45   *
46   */
47  public final class HgUtils {
48      private static final Logger LOGGER = LoggerFactory.getLogger(HgUtils.class);
49  
50      public static final String DEFAULT = "default";
51  
52      private HgUtils() {
53          // no op
54      }
55  
56      /**
57       * Map between command and its valid exit codes
58       */
59      private static final Map<String, List<Integer>> EXIT_CODE_MAP = new HashMap<String, List<Integer>>();
60  
61      /**
62       * Default exit codes for entries not in exitCodeMap
63       */
64      private static final List<Integer> DEFAULT_EXIT_CODES = new ArrayList<Integer>();
65  
66      /** Setup exit codes*/
67      static {
68          DEFAULT_EXIT_CODES.add(Integer.valueOf(0));
69  
70          // Diff is different
71          List<Integer> diffExitCodes = new ArrayList<Integer>(3);
72          diffExitCodes.add(Integer.valueOf(0)); // No difference
73          diffExitCodes.add(Integer.valueOf(1)); // Conflicts in merge-like or changes in diff-like
74          diffExitCodes.add(Integer.valueOf(2)); // Unrepresentable diff changes
75          EXIT_CODE_MAP.put(HgCommandConstants.DIFF_CMD, diffExitCodes);
76          // Outgoing is different
77          List<Integer> outgoingExitCodes = new ArrayList<Integer>(2);
78          outgoingExitCodes.add(Integer.valueOf(0)); // There are changes
79          outgoingExitCodes.add(Integer.valueOf(1)); // No changes
80          EXIT_CODE_MAP.put(HgCommandConstants.OUTGOING_CMD, outgoingExitCodes);
81      }
82  
83      public static ScmResult execute(HgConsumer consumer, File workingDir, String[] cmdAndArgs) throws ScmException {
84          try {
85              // Build commandline
86              Commandline cmd = buildCmd(workingDir, cmdAndArgs);
87              if (LOGGER.isInfoEnabled()) {
88                  LOGGER.info("EXECUTING: " + maskPassword(cmd));
89              }
90  
91              // Execute command
92              int exitCode = executeCmd(consumer, cmd);
93  
94              // Return result
95              List<Integer> exitCodes = DEFAULT_EXIT_CODES;
96              if (EXIT_CODE_MAP.containsKey(cmdAndArgs[0])) {
97                  exitCodes = EXIT_CODE_MAP.get(cmdAndArgs[0]);
98              }
99              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 }