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}