View Javadoc
1   package org.apache.maven.scm.provider.hg;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   * http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.scm.ScmException;
23  import org.apache.maven.scm.ScmFileSet;
24  import org.apache.maven.scm.ScmFileStatus;
25  import org.apache.maven.scm.ScmResult;
26  import org.apache.maven.scm.provider.hg.command.HgCommandConstants;
27  import org.apache.maven.scm.provider.hg.command.HgConsumer;
28  import org.apache.maven.scm.provider.hg.command.inventory.HgChangeSet;
29  import org.apache.maven.scm.provider.hg.command.inventory.HgOutgoingConsumer;
30  import org.codehaus.plexus.util.cli.CommandLineException;
31  import org.codehaus.plexus.util.cli.CommandLineUtils;
32  import org.codehaus.plexus.util.cli.Commandline;
33  import org.slf4j.Logger;
34  import org.slf4j.LoggerFactory;
35  
36  import java.io.File;
37  import java.util.ArrayList;
38  import java.util.HashMap;
39  import java.util.List;
40  import java.util.Map;
41  
42  /**
43   * Common code for executing hg commands.
44   *
45   * @author <a href="mailto:thurner.rupert@ymono.net">thurner rupert</a>
46   *
47   */
48  public final class HgUtils
49  {
50      private static final Logger LOGGER = LoggerFactory.getLogger( HgUtils.class );
51  
52      public static final String DEFAULT = "default";
53  
54      private HgUtils()
55      {
56          // no op
57      }
58  
59      /**
60       * Map between command and its valid exit codes
61       */
62      private static final Map<String, List<Integer>> EXIT_CODE_MAP = new HashMap<String, List<Integer>>();
63  
64      /**
65       * Default exit codes for entries not in exitCodeMap
66       */
67      private static final List<Integer> DEFAULT_EXIT_CODES = new ArrayList<Integer>();
68  
69      /** Setup exit codes*/
70      static
71      {
72          DEFAULT_EXIT_CODES.add( Integer.valueOf( 0 ) );
73  
74          //Diff is different
75          List<Integer> diffExitCodes = new ArrayList<Integer>( 3 );
76          diffExitCodes.add( Integer.valueOf( 0 ) ); //No difference
77          diffExitCodes.add( Integer.valueOf( 1 ) ); //Conflicts in merge-like or changes in diff-like
78          diffExitCodes.add( Integer.valueOf( 2 ) ); //Unrepresentable diff changes
79          EXIT_CODE_MAP.put( HgCommandConstants.DIFF_CMD, diffExitCodes );
80          //Outgoing is different
81          List<Integer> outgoingExitCodes = new ArrayList<Integer>( 2 );
82          outgoingExitCodes.add( Integer.valueOf( 0 ) ); //There are changes
83          outgoingExitCodes.add( Integer.valueOf( 1 ) ); //No changes
84          EXIT_CODE_MAP.put( HgCommandConstants.OUTGOING_CMD, outgoingExitCodes );
85      }
86  
87      public static ScmResult execute( HgConsumer consumer, File workingDir, String[] cmdAndArgs )
88          throws ScmException
89      {
90          try
91          {
92              //Build commandline
93              Commandline cmd = buildCmd( workingDir, cmdAndArgs );
94              if ( LOGGER.isInfoEnabled() )
95              {
96                  LOGGER.info( "EXECUTING: " + maskPassword( cmd ) );
97              }
98  
99              //Execute command
100             int exitCode = executeCmd( consumer, cmd );
101 
102             //Return result
103             List<Integer> exitCodes = DEFAULT_EXIT_CODES;
104             if ( EXIT_CODE_MAP.containsKey( cmdAndArgs[0] ) )
105             {
106                 exitCodes = EXIT_CODE_MAP.get( cmdAndArgs[0] );
107             }
108             boolean success = exitCodes.contains( Integer.valueOf( exitCode ) );
109 
110             //On failure (and not due to exceptions) - run diagnostics
111             String providerMsg = "Execution of hg command succeded";
112             if ( !success )
113             {
114                 HgConfig config = new HgConfig( workingDir );
115                 providerMsg =
116                     "\nEXECUTION FAILED" + "\n  Execution of cmd : " + cmdAndArgs[0] + " failed with exit code: "
117                         + exitCode + "." + "\n  Working directory was: " + "\n    " + workingDir.getAbsolutePath()
118                         + config.toString( workingDir ) + "\n";
119                 if ( LOGGER.isErrorEnabled() )
120                 {
121                     LOGGER.error( providerMsg );
122                 }
123             }
124 
125             return new ScmResult( cmd.toString(), providerMsg, consumer.getStdErr(), success );
126         }
127         catch ( ScmException se )
128         {
129             String msg =
130                 "EXECUTION FAILED" + "\n  Execution failed before invoking the Hg command. Last exception:" + "\n    "
131                     + se.getMessage();
132 
133             //Add nested cause if any
134             if ( se.getCause() != null )
135             {
136                 msg += "\n  Nested exception:" + "\n    " + se.getCause().getMessage();
137             }
138 
139             //log and return
140             if ( LOGGER.isErrorEnabled() )
141             {
142                 LOGGER.error( msg );
143             }
144             throw se;
145         }
146     }
147 
148     static Commandline buildCmd( File workingDir, String[] cmdAndArgs )
149         throws ScmException
150     {
151         Commandline cmd = new Commandline();
152         cmd.setExecutable( HgCommandConstants.EXEC );
153         cmd.addArguments( cmdAndArgs );
154         if ( workingDir != null )
155         {
156             cmd.setWorkingDirectory( workingDir.getAbsolutePath() );
157 
158             if ( !workingDir.exists() )
159             {
160                 boolean success = workingDir.mkdirs();
161                 if ( !success )
162                 {
163                     String msg = "Working directory did not exist" + " and it couldn't be created: " + workingDir;
164                     throw new ScmException( msg );
165                 }
166             }
167         }
168         return cmd;
169     }
170 
171     static int executeCmd( HgConsumer consumer, Commandline cmd )
172         throws ScmException
173     {
174         final int exitCode;
175         try
176         {
177             exitCode = CommandLineUtils.executeCommandLine( cmd, consumer, consumer );
178         }
179         catch ( CommandLineException ex )
180         {
181             throw new ScmException( "Command could not be executed: " + cmd, ex );
182         }
183         return exitCode;
184     }
185 
186     public static ScmResult execute( File workingDir, String[] cmdAndArgs )
187         throws ScmException
188     {
189         return execute( new HgConsumer(), 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( File workingDir )
213         throws ScmException
214     {
215 
216         String[] revCmd = new String[]{ HgCommandConstants.REVNO_CMD };
217         HgRevNoConsumer consumer = new HgRevNoConsumer();
218         HgUtils.execute( consumer, workingDir, revCmd );
219 
220         return consumer.getCurrentRevisionNumber();
221     }
222 
223     public static String getCurrentBranchName( File workingDir )
224         throws ScmException
225     {
226         String[] branchnameCmd = new String[]{ HgCommandConstants.BRANCH_NAME_CMD };
227         HgBranchnameConsumer consumer = new HgBranchnameConsumer();
228         HgUtils.execute( consumer, 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         public void doConsume( ScmFileStatus status, String line )
244         {
245             try
246             {
247                 revNo = Integer.valueOf( line ).intValue();
248             }
249             catch ( NumberFormatException e )
250             {
251                 // ignore
252             }
253         }
254 
255         int getCurrentRevisionNumber()
256         {
257             return revNo;
258         }
259     }
260 
261     /**
262      * Get current (working) branch name
263      */
264     private static class HgBranchnameConsumer
265         extends HgConsumer
266     {
267 
268         private String branchName;
269 
270         public void doConsume( ScmFileStatus status, String trimmedLine )
271         {
272             branchName = String.valueOf( trimmedLine );
273         }
274 
275         String getBranchName()
276         {
277             return branchName;
278         }
279 
280         /** {@inheritDoc} */
281         public void consumeLine( String line )
282         {
283             if ( logger.isDebugEnabled() )
284             {
285                 logger.debug( line );
286             }
287             String trimmedLine = line.trim();
288 
289             doConsume( null, trimmedLine );
290         }
291     }
292 
293 
294     /**
295      * Check if there are outgoing changes on a different branch. If so, Mercurial default behaviour
296      * is to block the push and warn using a 'push creates new remote branch !' message.
297      * We also warn, and return true if a different outgoing branch was found
298      * <p>
299      * Method users should not stop the push on a negative return, instead, they should
300      * hg push -r(branch being released)
301      *
302      * @param workingDir        the working dir
303      * @param workingbranchName the working branch name
304      * @return true if a different outgoing branch was found
305      * @throws ScmException on outgoing command error
306      */
307     public static boolean differentOutgoingBranchFound( File workingDir, String workingbranchName )
308         throws ScmException
309     {
310         String[] outCmd = new String[]{ HgCommandConstants.OUTGOING_CMD };
311         HgOutgoingConsumer outConsumer = new HgOutgoingConsumer();
312         ScmResult outResult = HgUtils.execute( outConsumer, workingDir, outCmd );
313         List<HgChangeSet> changes = outConsumer.getChanges();
314         if ( outResult.isSuccess() )
315         {
316             for ( HgChangeSet set : changes )
317             {
318                 if ( !getBranchName( workingbranchName ).equals( getBranchName( set.getBranch() ) ) )
319                 {
320                     LOGGER.warn( "A different branch than " + getBranchName( workingbranchName )
321                         + " was found in outgoing changes, branch name was " + getBranchName( set.getBranch() )
322                         + ". Only local branch named " + getBranchName( workingbranchName ) + " will be pushed." );
323                     return true;
324                 }
325             }
326         }
327         return false;
328     }
329 
330     private static String getBranchName( String branch )
331     {
332         return branch == null ? DEFAULT : branch;
333     }
334 
335     public static String maskPassword( Commandline cl )
336     {
337         String clString = cl.toString();
338 
339         int pos = clString.indexOf( '@' );
340 
341         if ( pos > 0 )
342         {
343             clString = clString.replaceAll( ":\\w+@", ":*****@" );
344         }
345 
346         return clString;
347     }
348 }