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