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    private HgUtils()
052    {
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<String, List<Integer>>();
059
060    /**
061     * Default exit codes for entries not in exitCodeMap
062     */
063    private static final List<Integer> DEFAULT_EXIT_CODES = new ArrayList<Integer>();
064
065    /** Setup exit codes*/
066    static
067    {
068        DEFAULT_EXIT_CODES.add( new Integer( 0 ) );
069
070        //Diff is different
071        List<Integer> diffExitCodes = new ArrayList<Integer>( 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<Integer>( 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, ScmLogger logger, File workingDir, String[] cmdAndArgs )
084        throws ScmException
085    {
086        try
087        {
088            //Build commandline
089            Commandline cmd = buildCmd( workingDir, cmdAndArgs );
090            if ( logger.isInfoEnabled() )
091            {
092                logger.info( "EXECUTING: " + maskPassword( cmd ) );
093            }
094
095            //Execute command
096            int exitCode = executeCmd( consumer, cmd );
097
098            //Return result
099            List<Integer> exitCodes = DEFAULT_EXIT_CODES;
100            if ( EXIT_CODE_MAP.containsKey( cmdAndArgs[0] ) )
101            {
102                exitCodes = EXIT_CODE_MAP.get( cmdAndArgs[0] );
103            }
104            boolean success = exitCodes.contains( Integer.valueOf( exitCode ) );
105
106            //On failure (and not due to exceptions) - run diagnostics
107            String providerMsg = "Execution of hg command succeded";
108            if ( !success )
109            {
110                HgConfig config = new HgConfig( workingDir );
111                providerMsg =
112                    "\nEXECUTION FAILED" + "\n  Execution of cmd : " + cmdAndArgs[0] + " failed with exit code: "
113                        + exitCode + "." + "\n  Working directory was: " + "\n    " + workingDir.getAbsolutePath()
114                        + config.toString( workingDir ) + "\n";
115                if ( logger.isErrorEnabled() )
116                {
117                    logger.error( providerMsg );
118                }
119            }
120
121            return new ScmResult( cmd.toString(), providerMsg, consumer.getStdErr(), success );
122        }
123        catch ( ScmException se )
124        {
125            String msg =
126                "EXECUTION FAILED" + "\n  Execution failed before invoking the Hg command. Last exception:" + "\n    "
127                    + se.getMessage();
128
129            //Add nested cause if any
130            if ( se.getCause() != null )
131            {
132                msg += "\n  Nested exception:" + "\n    " + se.getCause().getMessage();
133            }
134
135            //log and return
136            if ( logger.isErrorEnabled() )
137            {
138                logger.error( msg );
139            }
140            throw se;
141        }
142    }
143
144    static Commandline buildCmd( File workingDir, String[] cmdAndArgs )
145        throws ScmException
146    {
147        Commandline cmd = new Commandline();
148        cmd.setExecutable( HgCommandConstants.EXEC );
149        cmd.addArguments( cmdAndArgs );
150        if ( workingDir != null )
151        {
152            cmd.setWorkingDirectory( workingDir.getAbsolutePath() );
153
154            if ( !workingDir.exists() )
155            {
156                boolean success = workingDir.mkdirs();
157                if ( !success )
158                {
159                    String msg = "Working directory did not exist" + " and it couldn't be created: " + workingDir;
160                    throw new ScmException( msg );
161                }
162            }
163        }
164        return cmd;
165    }
166
167    static int executeCmd( HgConsumer consumer, Commandline cmd )
168        throws ScmException
169    {
170        final int exitCode;
171        try
172        {
173            exitCode = CommandLineUtils.executeCommandLine( cmd, consumer, consumer );
174        }
175        catch ( CommandLineException ex )
176        {
177            throw new ScmException( "Command could not be executed: " + cmd, ex );
178        }
179        return exitCode;
180    }
181
182    public static ScmResult execute( File workingDir, String[] cmdAndArgs )
183        throws ScmException
184    {
185        ScmLogger logger = new DefaultLog();
186        return execute( new HgConsumer( logger ), logger, workingDir, cmdAndArgs );
187    }
188
189    public static String[] expandCommandLine( String[] cmdAndArgs, ScmFileSet additionalFiles )
190    {
191        List<File> filesList = additionalFiles.getFileList();
192        String[] cmd = new String[filesList.size() + cmdAndArgs.length];
193
194        // Copy command into array
195        System.arraycopy( cmdAndArgs, 0, cmd, 0, cmdAndArgs.length );
196
197        // Add files as additional parameter into the array
198        int i = 0;
199        for ( File scmFile : filesList )
200        {
201            String file = scmFile.getPath().replace( '\\', File.separatorChar );
202            cmd[i + cmdAndArgs.length] = file;
203            i++;
204        }
205
206        return cmd;
207    }
208
209    public static int getCurrentRevisionNumber( ScmLogger logger, File workingDir )
210        throws ScmException
211    {
212
213        String[] revCmd = new String[]{ HgCommandConstants.REVNO_CMD };
214        HgRevNoConsumer consumer = new HgRevNoConsumer( logger );
215        HgUtils.execute( consumer, logger, workingDir, revCmd );
216
217        return consumer.getCurrentRevisionNumber();
218    }
219
220    public static String getCurrentBranchName( ScmLogger logger, File workingDir )
221        throws ScmException
222    {
223        String[] branchnameCmd = new String[]{ HgCommandConstants.BRANCH_NAME_CMD };
224        HgBranchnameConsumer consumer = new HgBranchnameConsumer( logger );
225        HgUtils.execute( consumer, logger, workingDir, branchnameCmd );
226        return consumer.getBranchName();
227    }
228
229    /**
230     * Get current (working) revision.
231     * <p/>
232     * Resolve revision to the last integer found in the command output.
233     */
234    private static class HgRevNoConsumer
235        extends HgConsumer
236    {
237
238        private int revNo;
239
240        HgRevNoConsumer( ScmLogger logger )
241        {
242            super( logger );
243        }
244
245        public void doConsume( ScmFileStatus status, String line )
246        {
247            try
248            {
249                revNo = Integer.valueOf( line ).intValue();
250            }
251            catch ( NumberFormatException e )
252            {
253                // ignore
254            }
255        }
256
257        int getCurrentRevisionNumber()
258        {
259            return revNo;
260        }
261    }
262
263    /**
264     * Get current (working) branch name
265     */
266    private static class HgBranchnameConsumer
267        extends HgConsumer
268    {
269
270        private String branchName;
271
272        HgBranchnameConsumer( ScmLogger logger )
273        {
274            super( logger );
275        }
276
277        public void doConsume( ScmFileStatus status, String trimmedLine )
278        {
279            branchName = String.valueOf( trimmedLine );
280        }
281
282        String getBranchName()
283        {
284            return branchName;
285        }
286    }
287
288
289    /**
290     * Check if there are outgoing changes on a different branch. If so, Mercurial default behaviour
291     * is to block the push and warn using a 'push creates new remote branch !' message.
292     * We also warn, and return true if a different outgoing branch was found
293     * <p/>
294     * Method users should not stop the push on a negative return, instead, they should hg push -r(branch being released)
295     *
296     * @param logger            the logger
297     * @param workingDir        the working dir
298     * @param workingbranchName the working branch name
299     * @return true if a different outgoing branch was found
300     * @throws ScmException on outgoing command error
301     */
302    public static boolean differentOutgoingBranchFound( ScmLogger logger, File workingDir, String workingbranchName )
303        throws ScmException
304    {
305        String[] outCmd = new String[]{ HgCommandConstants.OUTGOING_CMD };
306        HgOutgoingConsumer outConsumer = new HgOutgoingConsumer( logger );
307        ScmResult outResult = HgUtils.execute( outConsumer, logger, workingDir, outCmd );
308        List<HgChangeSet> changes = outConsumer.getChanges();
309        if ( outResult.isSuccess() )
310        {
311            for ( HgChangeSet set : changes )
312            {
313                if ( set.getBranch() != null )
314                {
315                    logger.warn( "A different branch than " + workingbranchName
316                        + " was found in outgoing changes, branch name was " + set.getBranch()
317                        + ". Only local branch named " + workingbranchName + " will be pushed." );
318                    return true;
319                }
320            }
321        }
322        return false;
323    }
324
325    public static String maskPassword( Commandline cl )
326    {
327        String clString = cl.toString();
328
329        int pos = clString.indexOf( '@' );
330
331        if ( pos > 0 )
332        {
333            clString = clString.replaceAll( ":\\w+@", ":*****@" );
334        }
335
336        return clString;
337    }
338}