001package org.apache.maven.scm.provider.hg;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 * http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.maven.scm.ScmException;
023import org.apache.maven.scm.ScmFileSet;
024import org.apache.maven.scm.ScmFileStatus;
025import org.apache.maven.scm.ScmResult;
026import org.apache.maven.scm.log.DefaultLog;
027import org.apache.maven.scm.log.ScmLogger;
028import org.apache.maven.scm.provider.hg.command.HgCommandConstants;
029import org.apache.maven.scm.provider.hg.command.HgConsumer;
030import org.apache.maven.scm.provider.hg.command.inventory.HgChangeSet;
031import org.apache.maven.scm.provider.hg.command.inventory.HgOutgoingConsumer;
032import org.codehaus.plexus.util.cli.CommandLineException;
033import org.codehaus.plexus.util.cli.CommandLineUtils;
034import org.codehaus.plexus.util.cli.Commandline;
035
036import java.io.File;
037import java.util.ArrayList;
038import java.util.HashMap;
039import java.util.List;
040import java.util.Map;
041
042/**
043 * Common code for executing hg commands.
044 *
045 * @author <a href="mailto:thurner.rupert@ymono.net">thurner rupert</a>
046 *
047 */
048public final class HgUtils
049{
050
051    public static final String DEFAULT = "default";
052
053    private HgUtils()
054    {
055        // no op
056    }
057
058    /**
059     * Map between command and its valid exit codes
060     */
061    private static final Map<String, List<Integer>> EXIT_CODE_MAP = new HashMap<String, List<Integer>>();
062
063    /**
064     * Default exit codes for entries not in exitCodeMap
065     */
066    private static final List<Integer> DEFAULT_EXIT_CODES = new ArrayList<Integer>();
067
068    /** Setup exit codes*/
069    static
070    {
071        DEFAULT_EXIT_CODES.add( Integer.valueOf( 0 ) );
072
073        //Diff is different
074        List<Integer> diffExitCodes = new ArrayList<Integer>( 3 );
075        diffExitCodes.add( Integer.valueOf( 0 ) ); //No difference
076        diffExitCodes.add( Integer.valueOf( 1 ) ); //Conflicts in merge-like or changes in diff-like
077        diffExitCodes.add( Integer.valueOf( 2 ) ); //Unrepresentable diff changes
078        EXIT_CODE_MAP.put( HgCommandConstants.DIFF_CMD, diffExitCodes );
079        //Outgoing is different
080        List<Integer> outgoingExitCodes = new ArrayList<Integer>( 2 );
081        outgoingExitCodes.add( Integer.valueOf( 0 ) ); //There are changes
082        outgoingExitCodes.add( Integer.valueOf( 1 ) ); //No changes
083        EXIT_CODE_MAP.put( HgCommandConstants.OUTGOING_CMD, outgoingExitCodes );
084    }
085
086    public static ScmResult execute( HgConsumer consumer, ScmLogger logger, File workingDir, String[] cmdAndArgs )
087        throws ScmException
088    {
089        try
090        {
091            //Build commandline
092            Commandline cmd = buildCmd( workingDir, cmdAndArgs );
093            if ( logger.isInfoEnabled() )
094            {
095                logger.info( "EXECUTING: " + maskPassword( cmd ) );
096            }
097
098            //Execute command
099            int exitCode = executeCmd( consumer, cmd );
100
101            //Return result
102            List<Integer> exitCodes = DEFAULT_EXIT_CODES;
103            if ( EXIT_CODE_MAP.containsKey( cmdAndArgs[0] ) )
104            {
105                exitCodes = EXIT_CODE_MAP.get( cmdAndArgs[0] );
106            }
107            boolean success = exitCodes.contains( Integer.valueOf( exitCode ) );
108
109            //On failure (and not due to exceptions) - run diagnostics
110            String providerMsg = "Execution of hg command succeded";
111            if ( !success )
112            {
113                HgConfig config = new HgConfig( workingDir );
114                providerMsg =
115                    "\nEXECUTION FAILED" + "\n  Execution of cmd : " + cmdAndArgs[0] + " failed with exit code: "
116                        + exitCode + "." + "\n  Working directory was: " + "\n    " + workingDir.getAbsolutePath()
117                        + config.toString( workingDir ) + "\n";
118                if ( logger.isErrorEnabled() )
119                {
120                    logger.error( providerMsg );
121                }
122            }
123
124            return new ScmResult( cmd.toString(), providerMsg, consumer.getStdErr(), success );
125        }
126        catch ( ScmException se )
127        {
128            String msg =
129                "EXECUTION FAILED" + "\n  Execution failed before invoking the Hg command. Last exception:" + "\n    "
130                    + se.getMessage();
131
132            //Add nested cause if any
133            if ( se.getCause() != null )
134            {
135                msg += "\n  Nested exception:" + "\n    " + se.getCause().getMessage();
136            }
137
138            //log and return
139            if ( logger.isErrorEnabled() )
140            {
141                logger.error( msg );
142            }
143            throw se;
144        }
145    }
146
147    static Commandline buildCmd( File workingDir, String[] cmdAndArgs )
148        throws ScmException
149    {
150        Commandline cmd = new Commandline();
151        cmd.setExecutable( HgCommandConstants.EXEC );
152        cmd.addArguments( cmdAndArgs );
153        if ( workingDir != null )
154        {
155            cmd.setWorkingDirectory( workingDir.getAbsolutePath() );
156
157            if ( !workingDir.exists() )
158            {
159                boolean success = workingDir.mkdirs();
160                if ( !success )
161                {
162                    String msg = "Working directory did not exist" + " and it couldn't be created: " + workingDir;
163                    throw new ScmException( msg );
164                }
165            }
166        }
167        return cmd;
168    }
169
170    static int executeCmd( HgConsumer consumer, Commandline cmd )
171        throws ScmException
172    {
173        final int exitCode;
174        try
175        {
176            exitCode = CommandLineUtils.executeCommandLine( cmd, consumer, consumer );
177        }
178        catch ( CommandLineException ex )
179        {
180            throw new ScmException( "Command could not be executed: " + cmd, ex );
181        }
182        return exitCode;
183    }
184
185    public static ScmResult execute( File workingDir, String[] cmdAndArgs )
186        throws ScmException
187    {
188        ScmLogger logger = new DefaultLog();
189        return execute( new HgConsumer( logger ), logger, workingDir, cmdAndArgs );
190    }
191
192    public static String[] expandCommandLine( String[] cmdAndArgs, ScmFileSet additionalFiles )
193    {
194        List<File> filesList = additionalFiles.getFileList();
195        String[] cmd = new String[filesList.size() + cmdAndArgs.length];
196
197        // Copy command into array
198        System.arraycopy( cmdAndArgs, 0, cmd, 0, cmdAndArgs.length );
199
200        // Add files as additional parameter into the array
201        int i = 0;
202        for ( File scmFile : filesList )
203        {
204            String file = scmFile.getPath().replace( '\\', File.separatorChar );
205            cmd[i + cmdAndArgs.length] = file;
206            i++;
207        }
208
209        return cmd;
210    }
211
212    public static int getCurrentRevisionNumber( ScmLogger logger, File workingDir )
213        throws ScmException
214    {
215
216        String[] revCmd = new String[]{ HgCommandConstants.REVNO_CMD };
217        HgRevNoConsumer consumer = new HgRevNoConsumer( logger );
218        HgUtils.execute( consumer, logger, workingDir, revCmd );
219
220        return consumer.getCurrentRevisionNumber();
221    }
222
223    public static String getCurrentBranchName( ScmLogger logger, File workingDir )
224        throws ScmException
225    {
226        String[] branchnameCmd = new String[]{ HgCommandConstants.BRANCH_NAME_CMD };
227        HgBranchnameConsumer consumer = new HgBranchnameConsumer( logger );
228        HgUtils.execute( consumer, logger, workingDir, branchnameCmd );
229        return consumer.getBranchName();
230    }
231
232    /**
233     * Get current (working) revision.
234     * <p/>
235     * Resolve revision to the last integer found in the command output.
236     */
237    private static class HgRevNoConsumer
238        extends HgConsumer
239    {
240
241        private int revNo;
242
243        HgRevNoConsumer( ScmLogger logger )
244        {
245            super( logger );
246        }
247
248        public void doConsume( ScmFileStatus status, String line )
249        {
250            try
251            {
252                revNo = Integer.valueOf( line ).intValue();
253            }
254            catch ( NumberFormatException e )
255            {
256                // ignore
257            }
258        }
259
260        int getCurrentRevisionNumber()
261        {
262            return revNo;
263        }
264    }
265
266    /**
267     * Get current (working) branch name
268     */
269    private static class HgBranchnameConsumer
270        extends HgConsumer
271    {
272
273        private String branchName;
274
275        HgBranchnameConsumer( ScmLogger logger )
276        {
277            super( logger );
278        }
279
280        public void doConsume( ScmFileStatus status, String trimmedLine )
281        {
282            branchName = String.valueOf( trimmedLine );
283        }
284
285        String getBranchName()
286        {
287            return branchName;
288        }
289
290        /** {@inheritDoc} */
291        public void consumeLine( String line )
292        {
293            if ( getLogger().isDebugEnabled() )
294            {
295                getLogger().debug( line );
296            }
297            String trimmedLine = line.trim();
298
299            doConsume( null, trimmedLine );
300        }
301    }
302
303
304    /**
305     * Check if there are outgoing changes on a different branch. If so, Mercurial default behaviour
306     * is to block the push and warn using a 'push creates new remote branch !' message.
307     * We also warn, and return true if a different outgoing branch was found
308     * <p/>
309     * Method users should not stop the push on a negative return, instead, they should
310     * hg push -r(branch being released)
311     *
312     * @param logger            the logger31
313     * @param workingDir        the working dir
314     * @param workingbranchName the working branch name
315     * @return true if a different outgoing branch was found
316     * @throws ScmException on outgoing command error
317     */
318    public static boolean differentOutgoingBranchFound( ScmLogger logger, File workingDir,String workingbranchName )
319        throws ScmException
320    {
321        String[] outCmd = new String[]{ HgCommandConstants.OUTGOING_CMD };
322        HgOutgoingConsumer outConsumer = new HgOutgoingConsumer( logger );
323        ScmResult outResult = HgUtils.execute( outConsumer, logger, workingDir, outCmd );
324        List<HgChangeSet> changes = outConsumer.getChanges();
325        if ( outResult.isSuccess() )
326        {
327            for ( HgChangeSet set : changes )
328            {
329                if (!getBranchName(workingbranchName).equals(getBranchName(set.getBranch()))) {
330                    logger.warn( "A different branch than " + getBranchName(workingbranchName)
331                        + " was found in outgoing changes, branch name was " + getBranchName(set.getBranch())
332                        + ". Only local branch named " + getBranchName(workingbranchName) + " will be pushed." );
333                    return true;
334                }
335            }
336        }
337        return false;
338    }
339
340    private static String getBranchName(String branch) {
341        return branch == null ? DEFAULT : branch;
342    }
343
344    public static String maskPassword( Commandline cl )
345    {
346        String clString = cl.toString();
347
348        int pos = clString.indexOf( '@' );
349
350        if ( pos > 0 )
351        {
352            clString = clString.replaceAll( ":\\w+@", ":*****@" );
353        }
354
355        return clString;
356    }
357}