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  public final class HgUtils {
47      private static final Logger LOGGER = LoggerFactory.getLogger(HgUtils.class);
48  
49      public static final String DEFAULT = "default";
50  
51      private HgUtils() {
52          // no op
53      }
54  
55      /**
56       * Map between command and its valid exit codes.
57       */
58      private static final Map<String, List<Integer>> EXIT_CODE_MAP = new HashMap<>();
59  
60      /**
61       * Default exit codes for entries not in exitCodeMap.
62       */
63      private static final List<Integer> DEFAULT_EXIT_CODES = new ArrayList<>();
64  
65      /** Setup exit codes. */
66      static {
67          DEFAULT_EXIT_CODES.add(Integer.valueOf(0));
68  
69          // Diff is different
70          List<Integer> diffExitCodes = new ArrayList<>(3);
71          diffExitCodes.add(Integer.valueOf(0)); // No difference
72          diffExitCodes.add(Integer.valueOf(1)); // Conflicts in merge-like or changes in diff-like
73          diffExitCodes.add(Integer.valueOf(2)); // Unrepresentable diff changes
74          EXIT_CODE_MAP.put(HgCommandConstants.DIFF_CMD, diffExitCodes);
75          // Outgoing is different
76          List<Integer> outgoingExitCodes = new ArrayList<>(2);
77          outgoingExitCodes.add(Integer.valueOf(0)); // There are changes
78          outgoingExitCodes.add(Integer.valueOf(1)); // No changes
79          EXIT_CODE_MAP.put(HgCommandConstants.OUTGOING_CMD, outgoingExitCodes);
80      }
81  
82      public static ScmResult execute(HgConsumer consumer, File workingDir, String[] cmdAndArgs) throws ScmException {
83          try {
84              // Build commandline
85              Commandline cmd = buildCmd(workingDir, cmdAndArgs);
86              if (LOGGER.isInfoEnabled()) {
87                  LOGGER.info("EXECUTING: " + maskPassword(cmd));
88              }
89  
90              // Execute command
91              int exitCode = executeCmd(consumer, cmd);
92  
93              // Return result
94              List<Integer> exitCodes = DEFAULT_EXIT_CODES;
95              if (EXIT_CODE_MAP.containsKey(cmdAndArgs[0])) {
96                  exitCodes = EXIT_CODE_MAP.get(cmdAndArgs[0]);
97              }
98              boolean success = exitCodes.contains(Integer.valueOf(exitCode));
99  
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 }