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}