001    package 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    
022    import org.apache.maven.scm.ScmException;
023    import org.apache.maven.scm.ScmFileSet;
024    import org.apache.maven.scm.ScmFileStatus;
025    import org.apache.maven.scm.ScmResult;
026    import org.apache.maven.scm.log.DefaultLog;
027    import org.apache.maven.scm.log.ScmLogger;
028    import org.apache.maven.scm.provider.hg.command.HgCommandConstants;
029    import org.apache.maven.scm.provider.hg.command.HgConsumer;
030    import org.apache.maven.scm.provider.hg.command.inventory.HgChangeSet;
031    import org.apache.maven.scm.provider.hg.command.inventory.HgOutgoingConsumer;
032    import org.codehaus.plexus.util.cli.CommandLineException;
033    import org.codehaus.plexus.util.cli.CommandLineUtils;
034    import org.codehaus.plexus.util.cli.Commandline;
035    
036    import java.io.File;
037    import java.util.ArrayList;
038    import java.util.HashMap;
039    import java.util.List;
040    import 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     * @version $Id: HgUtils.java 1378723 2012-08-29 20:57:26Z hboutemy $
047     */
048    public 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    }