001    package org.apache.maven.scm.provider.perforce;
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    
023    import java.io.BufferedReader;
024    import java.io.File;
025    import java.io.IOException;
026    import java.io.InputStreamReader;
027    import java.net.InetAddress;
028    import java.net.UnknownHostException;
029    
030    import org.apache.maven.scm.CommandParameters;
031    import org.apache.maven.scm.ScmException;
032    import org.apache.maven.scm.ScmFileSet;
033    import org.apache.maven.scm.command.add.AddScmResult;
034    import org.apache.maven.scm.command.blame.BlameScmResult;
035    import org.apache.maven.scm.command.changelog.ChangeLogScmResult;
036    import org.apache.maven.scm.command.checkin.CheckInScmResult;
037    import org.apache.maven.scm.command.checkout.CheckOutScmResult;
038    import org.apache.maven.scm.command.diff.DiffScmResult;
039    import org.apache.maven.scm.command.edit.EditScmResult;
040    import org.apache.maven.scm.command.login.LoginScmResult;
041    import org.apache.maven.scm.command.remove.RemoveScmResult;
042    import org.apache.maven.scm.command.status.StatusScmResult;
043    import org.apache.maven.scm.command.tag.TagScmResult;
044    import org.apache.maven.scm.command.unedit.UnEditScmResult;
045    import org.apache.maven.scm.command.update.UpdateScmResult;
046    import org.apache.maven.scm.log.ScmLogger;
047    import org.apache.maven.scm.provider.AbstractScmProvider;
048    import org.apache.maven.scm.provider.ScmProviderRepository;
049    import org.apache.maven.scm.provider.perforce.command.PerforceInfoCommand;
050    import org.apache.maven.scm.provider.perforce.command.PerforceWhereCommand;
051    import org.apache.maven.scm.provider.perforce.command.add.PerforceAddCommand;
052    import org.apache.maven.scm.provider.perforce.command.blame.PerforceBlameCommand;
053    import org.apache.maven.scm.provider.perforce.command.changelog.PerforceChangeLogCommand;
054    import org.apache.maven.scm.provider.perforce.command.checkin.PerforceCheckInCommand;
055    import org.apache.maven.scm.provider.perforce.command.checkout.PerforceCheckOutCommand;
056    import org.apache.maven.scm.provider.perforce.command.diff.PerforceDiffCommand;
057    import org.apache.maven.scm.provider.perforce.command.edit.PerforceEditCommand;
058    import org.apache.maven.scm.provider.perforce.command.login.PerforceLoginCommand;
059    import org.apache.maven.scm.provider.perforce.command.remove.PerforceRemoveCommand;
060    import org.apache.maven.scm.provider.perforce.command.status.PerforceStatusCommand;
061    import org.apache.maven.scm.provider.perforce.command.tag.PerforceTagCommand;
062    import org.apache.maven.scm.provider.perforce.command.unedit.PerforceUnEditCommand;
063    import org.apache.maven.scm.provider.perforce.command.update.PerforceUpdateCommand;
064    import org.apache.maven.scm.provider.perforce.repository.PerforceScmProviderRepository;
065    import org.apache.maven.scm.repository.ScmRepositoryException;
066    import org.codehaus.plexus.util.StringUtils;
067    import org.codehaus.plexus.util.cli.Commandline;
068    
069    /**
070     * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l </a>
071     * @author mperham
072     * @version $Id: PerforceScmProvider.java 1306867 2012-03-29 13:45:10Z olamy $
073     * @plexus.component role="org.apache.maven.scm.provider.ScmProvider" role-hint="perforce"
074     */
075    public class PerforceScmProvider
076        extends AbstractScmProvider
077    {
078        // ----------------------------------------------------------------------
079        // ScmProvider Implementation
080        // ----------------------------------------------------------------------
081    
082        public boolean requiresEditMode()
083        {
084            return true;
085        }
086    
087        public ScmProviderRepository makeProviderScmRepository( String scmSpecificUrl, char delimiter )
088            throws ScmRepositoryException
089        {
090            String path;
091            int port = 0;
092            String host = null;
093    
094            int i1 = scmSpecificUrl.indexOf( delimiter );
095            int i2 = scmSpecificUrl.indexOf( delimiter, i1 + 1 );
096    
097            if ( i1 > 0 )
098            {
099                int lastDelimiter = scmSpecificUrl.lastIndexOf( delimiter );
100                path = scmSpecificUrl.substring( lastDelimiter + 1 );
101                host = scmSpecificUrl.substring( 0, i1 );
102    
103                // If there is tree parts in the scm url, the second is the port
104                if ( i2 >= 0 )
105                {
106                    try
107                    {
108                        String tmp = scmSpecificUrl.substring( i1 + 1, lastDelimiter );
109                        port = Integer.parseInt( tmp );
110                    }
111                    catch ( NumberFormatException ex )
112                    {
113                        throw new ScmRepositoryException( "The port has to be a number." );
114                    }
115                }
116            }
117            else
118            {
119                path = scmSpecificUrl;
120            }
121    
122            String user = null;
123            String password = null;
124            if ( host != null && host.indexOf( '@' ) > 1 )
125            {
126                user = host.substring( 0, host.indexOf( '@' ) );
127                host = host.substring( host.indexOf( '@' ) + 1 );
128            }
129    
130            if ( path.indexOf( '@' ) > 1 )
131            {
132                if ( host != null )
133                {
134                    if ( getLogger().isWarnEnabled() )
135                    {
136                        getLogger().warn(
137                                          "Username as part of path is deprecated, the new format is "
138                                              + "scm:perforce:[username@]host:port:path_to_repository" );
139                    }
140                }
141    
142                user = path.substring( 0, path.indexOf( '@' ) );
143                path = path.substring( path.indexOf( '@' ) + 1 );
144            }
145    
146            return new PerforceScmProviderRepository( host, port, path, user, password );
147        }
148    
149        public String getScmType()
150        {
151            return "perforce";
152        }
153    
154        /** {@inheritDoc} */
155        protected ChangeLogScmResult changelog( ScmProviderRepository repository, ScmFileSet fileSet,
156                                                CommandParameters parameters )
157            throws ScmException
158        {
159            PerforceChangeLogCommand command = new PerforceChangeLogCommand();
160            command.setLogger( getLogger() );
161            return (ChangeLogScmResult) command.execute( repository, fileSet, parameters );
162        }
163    
164        public AddScmResult add( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
165            throws ScmException
166        {
167            PerforceAddCommand command = new PerforceAddCommand();
168            command.setLogger( getLogger() );
169            return (AddScmResult) command.execute( repository, fileSet, params );
170        }
171    
172        protected RemoveScmResult remove( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
173            throws ScmException
174        {
175            PerforceRemoveCommand command = new PerforceRemoveCommand();
176            command.setLogger( getLogger() );
177            return (RemoveScmResult) command.execute( repository, fileSet, params );
178        }
179    
180        protected CheckInScmResult checkin( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
181            throws ScmException
182        {
183            PerforceCheckInCommand command = new PerforceCheckInCommand();
184            command.setLogger( getLogger() );
185            return (CheckInScmResult) command.execute( repository, fileSet, params );
186        }
187    
188        protected CheckOutScmResult checkout( ScmProviderRepository repository, ScmFileSet fileSet,
189                                              CommandParameters params )
190            throws ScmException
191        {
192            PerforceCheckOutCommand command = new PerforceCheckOutCommand();
193            command.setLogger( getLogger() );
194            return (CheckOutScmResult) command.execute( repository, fileSet, params );
195        }
196    
197        protected DiffScmResult diff( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
198            throws ScmException
199        {
200            PerforceDiffCommand command = new PerforceDiffCommand();
201            command.setLogger( getLogger() );
202            return (DiffScmResult) command.execute( repository, fileSet, params );
203        }
204    
205        protected EditScmResult edit( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
206            throws ScmException
207        {
208            PerforceEditCommand command = new PerforceEditCommand();
209            command.setLogger( getLogger() );
210            return (EditScmResult) command.execute( repository, fileSet, params );
211        }
212    
213        protected LoginScmResult login( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
214            throws ScmException
215        {
216            PerforceLoginCommand command = new PerforceLoginCommand();
217            command.setLogger( getLogger() );
218            return (LoginScmResult) command.execute( repository, fileSet, params );
219        }
220    
221        protected StatusScmResult status( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
222            throws ScmException
223        {
224            PerforceStatusCommand command = new PerforceStatusCommand();
225            command.setLogger( getLogger() );
226            return (StatusScmResult) command.execute( repository, fileSet, params );
227        }
228    
229        protected TagScmResult tag( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
230            throws ScmException
231        {
232            PerforceTagCommand command = new PerforceTagCommand();
233            command.setLogger( getLogger() );
234            return (TagScmResult) command.execute( repository, fileSet, params );
235        }
236    
237        protected UnEditScmResult unedit( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
238            throws ScmException
239        {
240            PerforceUnEditCommand command = new PerforceUnEditCommand();
241            command.setLogger( getLogger() );
242            return (UnEditScmResult) command.execute( repository, fileSet, params );
243        }
244    
245        protected UpdateScmResult update( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
246            throws ScmException
247        {
248            PerforceUpdateCommand command = new PerforceUpdateCommand();
249            command.setLogger( getLogger() );
250            return (UpdateScmResult) command.execute( repository, fileSet, params );
251        }
252    
253        protected BlameScmResult blame( ScmProviderRepository repository, ScmFileSet fileSet, CommandParameters params )
254            throws ScmException
255        {
256            PerforceBlameCommand command = new PerforceBlameCommand();
257            command.setLogger( getLogger() );
258            return (BlameScmResult) command.execute( repository, fileSet, params );
259        }
260    
261        public static Commandline createP4Command( PerforceScmProviderRepository repo, File workingDir )
262        {
263            Commandline command = new Commandline();
264            command.setExecutable( "p4" );
265            if ( workingDir != null )
266            {
267                // SCM-209
268                command.createArg().setValue( "-d" );
269                command.createArg().setValue( workingDir.getAbsolutePath() );
270            }
271    
272            if ( repo.getHost() != null )
273            {
274                command.createArg().setValue( "-p" );
275                String value = repo.getHost();
276                if ( repo.getPort() != 0 )
277                {
278                    value += ":" + Integer.toString( repo.getPort() );
279                }
280                command.createArg().setValue( value );
281            }
282    
283            if ( StringUtils.isNotEmpty( repo.getUser() ) )
284            {
285                command.createArg().setValue( "-u" );
286                command.createArg().setValue( repo.getUser() );
287            }
288    
289            if ( StringUtils.isNotEmpty( repo.getPassword() ) )
290            {
291                command.createArg().setValue( "-P" );
292                command.createArg().setValue( repo.getPassword() );
293            }
294            return command;
295        }
296    
297        public static String clean( String string )
298        {
299            if ( string.indexOf( " -P " ) == -1 )
300            {
301                return string;
302            }
303            int idx = string.indexOf( " -P " ) + 4;
304            int end = string.indexOf( ' ', idx );
305            return string.substring( 0, idx ) + StringUtils.repeat( "*", end - idx ) + string.substring( end );
306        }
307    
308        /**
309         * Given a path like "//depot/foo/bar", returns the
310         * proper path to include everything beneath it.
311         * <p/>
312         * //depot/foo/bar -> //depot/foo/bar/...
313         * //depot/foo/bar/ -> //depot/foo/bar/...
314         * //depot/foo/bar/... -> //depot/foo/bar/...
315         *
316         * @param repoPath
317         * @return
318         */
319        public static String getCanonicalRepoPath( String repoPath )
320        {
321            if ( repoPath.endsWith( "/..." ) )
322            {
323                return repoPath;
324            }
325            else if ( repoPath.endsWith( "/" ) )
326            {
327                return repoPath + "...";
328            }
329            else
330            {
331                return repoPath + "/...";
332            }
333        }
334    
335        private static final String NEWLINE = "\r\n";
336    
337        /*
338         * Clientspec name can be overridden with the system property below.  I don't
339         * know of any way for this code to get access to maven's settings.xml so this
340         * is the best I can do.
341         *
342         * Sample clientspec:
343    
344         Client: mperham-mikeperham-dt-maven
345         Root: d:\temp\target
346         Owner: mperham
347         View:
348         //depot/sandbox/mperham/tsa/tsa-domain/... //mperham-mikeperham-dt-maven/...
349         Description:
350         Created by maven-scm-provider-perforce
351    
352         */
353        public static String createClientspec( ScmLogger logger, PerforceScmProviderRepository repo, File workDir,
354                                               String repoPath )
355        {
356            String clientspecName = getClientspecName( logger, repo, workDir );
357            String userName = getUsername( logger, repo );
358    
359            String rootDir;
360            try
361            {
362                // SCM-184
363                rootDir = workDir.getCanonicalPath();
364            }
365            catch ( IOException ex )
366            {
367                //getLogger().error("Error getting canonical path for working directory: " + workDir, ex);
368                rootDir = workDir.getAbsolutePath();
369            }
370    
371            StringBuilder buf = new StringBuilder();
372            buf.append( "Client: " ).append( clientspecName ).append( NEWLINE );
373            buf.append( "Root: " ).append( rootDir ).append( NEWLINE );
374            buf.append( "Owner: " ).append( userName ).append( NEWLINE );
375            buf.append( "View:" ).append( NEWLINE );
376            buf.append( "\t" ).append( PerforceScmProvider.getCanonicalRepoPath( repoPath ) );
377            buf.append( " //" ).append( clientspecName ).append( "/..." ).append( NEWLINE );
378            buf.append( "Description:" ).append( NEWLINE );
379            buf.append( "\t" ).append( "Created by maven-scm-provider-perforce" ).append( NEWLINE );
380            return buf.toString();
381        }
382    
383        public static final String DEFAULT_CLIENTSPEC_PROPERTY = "maven.scm.perforce.clientspec.name";
384    
385        public static String getClientspecName( ScmLogger logger, PerforceScmProviderRepository repo, File workDir )
386        {
387            String def = generateDefaultClientspecName( logger, repo, workDir );
388            // until someone put clearProperty in DefaultContinuumScm.getScmRepository( Project , boolean  )
389            String l = System.getProperty( DEFAULT_CLIENTSPEC_PROPERTY, def );
390            if ( l == null || "".equals( l.trim() ) )
391            {
392                return def;
393            }
394            return l;
395        }
396    
397        private static String generateDefaultClientspecName( ScmLogger logger, PerforceScmProviderRepository repo,
398                                                             File workDir )
399        {
400            String username = getUsername( logger, repo );
401            String hostname;
402            String path;
403            try
404            {
405                hostname = InetAddress.getLocalHost().getHostName();
406                // [SCM-370][SCM-351] client specs cannot contain forward slashes, spaces and ~; "-" is okay
407                path = workDir.getCanonicalPath().replaceAll( "[/ ~]", "-" );
408            }
409            catch ( UnknownHostException e )
410            {
411                // Should never happen
412                throw new RuntimeException( e );
413            }
414            catch ( IOException e )
415            {
416                throw new RuntimeException( e );
417            }
418            return username + "-" + hostname + "-MavenSCM-" + path;
419        }
420    
421        private static String getUsername( ScmLogger logger, PerforceScmProviderRepository repo )
422        {
423            String username = PerforceInfoCommand.getInfo( logger, repo ).getEntry( "User name" );
424            if ( username == null )
425            {
426                // os user != perforce user
427                username = repo.getUser();
428                if ( username == null )
429                {
430                    username = System.getProperty( "user.name", "nouser" );
431                }
432            }
433            return username;
434        }
435    
436        /**
437         * This is a "safe" method which handles cases where repo.getPath() is
438         * not actually a valid Perforce depot location.  This is a frequent error
439         * due to branches and directory naming where dir name != artifactId.
440         *
441         * @param log     the logging object to use
442         * @param repo    the Perforce repo
443         * @param basedir the base directory we are operating in.  If pom.xml exists in this directory,
444         *                this method will verify <pre>repo.getPath()/pom.xml</pre> == <pre>p4 where basedir/pom.xml</pre>
445         * @return repo.getPath if it is determined to be accurate.  The p4 where location otherwise.
446         */
447        public static String getRepoPath( ScmLogger log, PerforceScmProviderRepository repo, File basedir )
448        {
449            PerforceWhereCommand where = new PerforceWhereCommand( log, repo );
450    
451            // Handle an edge case where we release:prepare'd a module with an invalid SCM location.
452            // In this case, the release.properties will contain the invalid URL for checkout purposes
453            // during release:perform.  In this case, the basedir is not the module root so we detect that
454            // and remove the trailing target/checkout directory.
455            if ( basedir.toString().replace( '\\', '/' ).endsWith( "/target/checkout" ) )
456            {
457                String dir = basedir.toString();
458                basedir = new File( dir.substring( 0, dir.length() - "/target/checkout".length() ) );
459                log.debug( "Fixing checkout URL: " + basedir );
460            }
461            File pom = new File( basedir, "pom.xml" );
462            String loc = repo.getPath();
463            log.debug( "SCM path in pom: " + loc );
464            if ( pom.exists() )
465            {
466                loc = where.getDepotLocation( pom );
467                if ( loc == null )
468                {
469                    loc = repo.getPath();
470                    log.debug( "cannot find depot => using " + loc );
471                }
472                else if ( loc.endsWith( "/pom.xml" ) )
473                {
474                    loc = loc.substring( 0, loc.length() - "/pom.xml".length() );
475                    log.debug( "Actual POM location: " + loc );
476                    if ( !repo.getPath().equals( loc ) )
477                    {
478                        log.info( "The SCM location in your pom.xml (" + repo.getPath()
479                            + ") is not equal to the depot location (" + loc
480                            + ").  This happens frequently with branches.  " + "Ignoring the SCM location." );
481                    }
482                }
483            }
484            return loc;
485        }
486    
487    
488        private static Boolean live = null;
489    
490        public static boolean isLive()
491        {
492            if ( live == null )
493            {
494                if ( !Boolean.getBoolean( "maven.scm.testing" ) )
495                {
496                    // We are not executing in the tests so we are live.
497                    live = Boolean.TRUE;
498                }
499                else
500                {
501                    // During unit tests, we need to check the local system
502                    // to see if the user has Perforce installed.  If not, we mark
503                    // the provider as "not live" (or dead, I suppose!) and skip
504                    // anything that requires an active server connection.
505                    try
506                    {
507                        Commandline command = new Commandline();
508                        command.setExecutable( "p4" );
509                        Process proc = command.execute();
510                        BufferedReader br = new BufferedReader( new InputStreamReader( proc.getInputStream() ) );
511                        @SuppressWarnings( "unused" )
512                        String line;
513                        while ( ( line = br.readLine() ) != null )
514                        {
515                            //System.out.println(line);
516                        }
517                        int rc = proc.exitValue();
518                        live = ( rc == 0 ? Boolean.TRUE : Boolean.FALSE );
519                    }
520                    catch ( Exception e )
521                    {
522                        e.printStackTrace();
523                        live = Boolean.FALSE;
524                    }
525                }
526            }
527    
528            return live.booleanValue();
529        }
530    }