001    package org.apache.maven.scm.provider.accurev.command.changelog;
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 java.util.ArrayList;
023    import java.util.Collection;
024    import java.util.Collections;
025    import java.util.Date;
026    import java.util.HashMap;
027    import java.util.List;
028    import java.util.Map;
029    
030    import org.apache.maven.scm.ChangeFile;
031    import org.apache.maven.scm.ChangeSet;
032    import org.apache.maven.scm.CommandParameter;
033    import org.apache.maven.scm.CommandParameters;
034    import org.apache.maven.scm.ScmBranch;
035    import org.apache.maven.scm.ScmException;
036    import org.apache.maven.scm.ScmFileSet;
037    import org.apache.maven.scm.ScmResult;
038    import org.apache.maven.scm.ScmRevision;
039    import org.apache.maven.scm.ScmVersion;
040    import org.apache.maven.scm.command.changelog.ChangeLogScmResult;
041    import org.apache.maven.scm.command.changelog.ChangeLogSet;
042    import org.apache.maven.scm.log.ScmLogger;
043    import org.apache.maven.scm.provider.ScmProviderRepository;
044    import org.apache.maven.scm.provider.accurev.AccuRev;
045    import org.apache.maven.scm.provider.accurev.AccuRevCapability;
046    import org.apache.maven.scm.provider.accurev.AccuRevException;
047    import org.apache.maven.scm.provider.accurev.AccuRevScmProviderRepository;
048    import org.apache.maven.scm.provider.accurev.AccuRevVersion;
049    import org.apache.maven.scm.provider.accurev.FileDifference;
050    import org.apache.maven.scm.provider.accurev.Stream;
051    import org.apache.maven.scm.provider.accurev.Transaction;
052    import org.apache.maven.scm.provider.accurev.Transaction.Version;
053    import org.apache.maven.scm.provider.accurev.command.AbstractAccuRevCommand;
054    import org.codehaus.plexus.util.StringUtils;
055    
056    /**
057     * TODO filter results based on project_path Find appropriate start and end transaction ids from parameters. Streams
058     * must be the same. Diff on stream start to end - these are the upstream changes Hist on the stream start+1 to end
059     * remove items from the upstream set if they appear in the history For workspaces diff doesn't work. So we would not
060     * pickup any upstream changes, just the "keep" transactions which is not very useful. Hist on the workspace Then diff /
061     * hist on the basis stream, skipping any transactions that are coming from the workspace.
062     * 
063     * @author ggardner
064     */
065    public class AccuRevChangeLogCommand
066        extends AbstractAccuRevCommand
067    {
068    
069        public AccuRevChangeLogCommand( ScmLogger logger )
070        {
071    
072            super( logger );
073        }
074    
075        @Override
076        protected ScmResult executeAccurevCommand( AccuRevScmProviderRepository repository, ScmFileSet fileSet,
077                                                   CommandParameters parameters )
078            throws ScmException, AccuRevException
079        {
080    
081            // Do we have a supplied branch. If not we default to the URL stream.
082            ScmBranch branch = (ScmBranch) parameters.getScmVersion( CommandParameter.BRANCH, null );
083            AccuRevVersion branchVersion = repository.getAccuRevVersion( branch );
084            String stream = branchVersion.getBasisStream();
085            String fromSpec = branchVersion.getTimeSpec();
086            String toSpec = "highest";
087    
088            // Versions
089            ScmVersion startVersion = parameters.getScmVersion( CommandParameter.START_SCM_VERSION, null );
090            ScmVersion endVersion = parameters.getScmVersion( CommandParameter.END_SCM_VERSION, null );
091    
092            if ( startVersion != null && StringUtils.isNotEmpty( startVersion.getName() ) )
093            {
094                AccuRevVersion fromVersion = repository.getAccuRevVersion( startVersion );
095                // if no end version supplied then use same basis as startVersion
096                AccuRevVersion toVersion =
097                    endVersion == null ? new AccuRevVersion( fromVersion.getBasisStream(), "now" )
098                                    : repository.getAccuRevVersion( endVersion );
099                    
100                if ( !StringUtils.equals( fromVersion.getBasisStream(), toVersion.getBasisStream() ) )
101                {
102                    throw new AccuRevException( "Not able to provide change log between different streams " + fromVersion
103                        + "," + toVersion );
104                }
105    
106                stream = fromVersion.getBasisStream();
107                fromSpec = fromVersion.getTimeSpec();
108                toSpec = toVersion.getTimeSpec();
109    
110            }
111    
112            Date startDate = parameters.getDate( CommandParameter.START_DATE, null );
113            Date endDate = parameters.getDate( CommandParameter.END_DATE, null );
114            int numDays = parameters.getInt( CommandParameter.NUM_DAYS, 0 );
115    
116            if ( numDays > 0 )
117            {
118                if ( ( startDate != null || endDate != null ) )
119                {
120                    throw new ScmException( "Start or end date cannot be set if num days is set." );
121                }
122                // Last x days.
123                int day = 24 * 60 * 60 * 1000;
124                startDate = new Date( System.currentTimeMillis() - (long) numDays * day );
125                endDate = new Date( System.currentTimeMillis() + day );
126            }
127    
128            if ( endDate != null && startDate == null )
129            {
130                throw new ScmException( "The end date is set but the start date isn't." );
131            }
132    
133            // Date parameters override transaction ids in versions
134            if ( startDate != null )
135            {
136                fromSpec = AccuRevScmProviderRepository.formatTimeSpec( startDate );
137            }
138            else if ( fromSpec == null )
139            {
140                fromSpec = "1";
141            }
142    
143            // Convert the fromSpec to both a date AND a transaction id by looking up
144            // the nearest transaction in the depot.
145            Transaction fromTransaction = getDepotTransaction( repository, stream, fromSpec );
146    
147            long fromTranId = 1;
148            if ( fromTransaction != null )
149            {
150                // This tran id is less than or equal to the date/tranid we requested.
151                fromTranId = fromTransaction.getTranId();
152                if ( startDate == null )
153                {
154                    startDate = fromTransaction.getWhen();
155                }
156            }
157    
158            if ( endDate != null )
159            {
160                toSpec = AccuRevScmProviderRepository.formatTimeSpec( endDate );
161            }
162            else if ( toSpec == null )
163            {
164                toSpec = "highest";
165            }
166    
167            Transaction toTransaction = getDepotTransaction( repository, stream, toSpec );
168            long toTranId = 1;
169            if ( toTransaction != null )
170            {
171                toTranId = toTransaction.getTranId();
172                if ( endDate == null )
173                {
174                    endDate = toTransaction.getWhen();
175                }
176            }
177            startVersion = new ScmRevision( repository.getRevision( stream, fromTranId ) );
178            endVersion = new ScmRevision( repository.getRevision( stream, toTranId ) );
179    
180            //TODO Split this method in two here. above to convert params to start and end (stream,tranid,date) and test independantly
181            
182            List<Transaction> streamHistory = Collections.emptyList();
183            List<Transaction> workspaceHistory = Collections.emptyList();
184            List<FileDifference> streamDifferences = Collections.emptyList();
185    
186            StringBuilder errorMessage = new StringBuilder();
187    
188            AccuRev accurev = repository.getAccuRev();
189    
190            Stream changelogStream = accurev.showStream( stream );
191            if ( changelogStream == null )
192            {
193                errorMessage.append( "Unknown accurev stream -" ).append( stream ).append( "." );
194            }
195            else
196            {
197    
198                String message =
199                    "Changelog on stream " + stream + "(" + changelogStream.getStreamType() + ") from " + fromTranId + " ("
200                        + startDate + "), to " + toTranId + " (" + endDate + ")";
201    
202                if ( startDate != null && startDate.after( endDate ) || fromTranId >= toTranId )
203                {
204                    getLogger().warn( "Skipping out of range " + message );
205                }
206                else
207                {
208    
209                    getLogger().info( message );
210    
211                    // In 4.7.2 and higher we have a diff command that will list all the file differences in a stream
212                    // and thus can be used to detect upstream changes
213                    // Unfortunately diff -v -V -t does not work in workspaces.
214                    Stream diffStream = changelogStream;
215                    if ( changelogStream.isWorkspace() )
216                    {
217    
218                        workspaceHistory =
219                            accurev.history( stream, Long.toString( fromTranId + 1 ), Long.toString( toTranId ), 0, false,
220                                             false );
221    
222                        if ( workspaceHistory == null )
223                        {
224                            errorMessage.append( "history on workspace " + stream + " from " + fromTranId + 1 + " to "
225                                + toTranId + " failed." );
226    
227                        }
228    
229                        // do the diff/hist on the basis stream instead.
230                        stream = changelogStream.getBasis();
231                        diffStream = accurev.showStream( stream );
232    
233                    }
234    
235                    if ( AccuRevCapability.DIFF_BETWEEN_STREAMS.isSupported( accurev.getClientVersion() ) )
236                    {
237                        if ( startDate.before( diffStream.getStartDate() ) )
238                        {
239                            getLogger().warn( "Skipping diff of " + stream + " due to start date out of range" );
240                        }
241                        else
242                        {
243                            streamDifferences =
244                                accurev.diff( stream, Long.toString( fromTranId ), Long.toString( toTranId ) );
245                            if ( streamDifferences == null )
246                            {
247                                errorMessage.append( "Diff " + stream + "- " + fromTranId + " to " + toTranId + "failed." );
248                            }
249                        }
250                    }
251    
252                    // History needs to start from the transaction after our starting transaction
253    
254                    streamHistory =
255                        accurev.history( stream, Long.toString( fromTranId + 1 ), Long.toString( toTranId ), 0, false,
256                                         false );
257                    if ( streamHistory == null )
258                    {
259                        errorMessage.append( "history on stream " + stream + " from " + fromTranId + 1 + " to " + toTranId
260                            + " failed." );
261                    }
262    
263                }
264            }
265    
266            String errorString = errorMessage.toString();
267            if ( StringUtils.isBlank( errorString ) )
268            {
269                ChangeLogSet changeLog =
270                    getChangeLog( changelogStream, streamDifferences, streamHistory, workspaceHistory, startDate, endDate );
271    
272                changeLog.setEndVersion( endVersion );
273                changeLog.setStartVersion( startVersion );
274    
275                return new ChangeLogScmResult( accurev.getCommandLines(), changeLog );
276            }
277            else
278            {
279                return new ChangeLogScmResult( accurev.getCommandLines(), "AccuRev errors: " + errorMessage,
280                                               accurev.getErrorOutput(), false );
281            }
282    
283        }
284    
285        private Transaction getDepotTransaction( AccuRevScmProviderRepository repo, String stream, String tranSpec )
286            throws AccuRevException
287        {
288            return repo.getDepotTransaction( stream, tranSpec );
289    
290        }
291    
292        private ChangeLogSet getChangeLog( Stream stream, List<FileDifference> streamDifferences,
293                                           List<Transaction> streamHistory, List<Transaction> workspaceHistory,
294                                           Date startDate, Date endDate )
295        {
296    
297            // Collect all the "to" versions from the streamDifferences into a Map by element id
298            // If that version is seen in the promote/keep history then we move it from the map
299            // At the end we create a pseudo ChangeSet for any remaining entries in the map as
300            // representing "upstream changes"
301            Map<Long, FileDifference> differencesMap = new HashMap<Long, FileDifference>();
302            for ( FileDifference fileDifference : streamDifferences )
303            {
304                differencesMap.put( fileDifference.getElementId(), fileDifference );
305            }
306    
307            List<Transaction> mergedHistory = new ArrayList<Transaction>( streamHistory );
308            // will never match a version
309            String streamPrefix = "/";
310    
311            mergedHistory.addAll( workspaceHistory );
312            streamPrefix = stream.getId() + "/";
313    
314            List<ChangeSet> entries = new ArrayList<ChangeSet>( streamHistory.size() );
315            for ( Transaction t : mergedHistory )
316            {
317                if ( ( startDate != null && t.getWhen().before( startDate ) )
318                    || ( endDate != null && t.getWhen().after( endDate ) ) )
319                {
320                    // This is possible if dates and transactions are mixed in the time spec.
321                    continue;
322                }
323    
324                // Needed to make Tck test pass against accurev > 4.7.2 - the changelog only expects to deal with
325                // files. Stream changes and cross links are important entries in the changelog.
326                // However we should only see mkstream once and it is irrelevant given we are interrogating
327                // the history of this stream.
328                if ( "mkstream".equals( t.getTranType() ) )
329                {
330                    continue;
331                }
332    
333                Collection<Version> versions = t.getVersions();
334                List<ChangeFile> files = new ArrayList<ChangeFile>( versions.size() );
335    
336                for ( Version v : versions )
337                {
338    
339                    // Remove diff representing this promote
340                    FileDifference difference = differencesMap.get( v.getElementId() );
341                    // TODO: how are defuncts shown in the version history?
342                    if ( difference != null )
343                    {
344                        String newVersionSpec = difference.getNewVersionSpec();
345                        if ( newVersionSpec != null && newVersionSpec.equals( v.getRealSpec() ) )
346                        {
347                            if ( getLogger().isDebugEnabled() )
348                            {
349                                getLogger().debug( "Removing difference for " + v );
350                            }
351                            differencesMap.remove( v.getElementId() );
352                        }
353                    }
354    
355                    // Add this file, unless the virtual version indicates this is the basis stream, and the real
356                    // version came from our workspace stream (ie, this transaction is a promote from the workspace
357                    // to its basis stream, and is therefore NOT a change
358                    if ( v.getRealSpec().startsWith( streamPrefix ) && !v.getVirtualSpec().startsWith( streamPrefix ) )
359                    {
360                        if ( getLogger().isDebugEnabled() )
361                        {
362                            getLogger().debug( "Skipping workspace to basis stream promote " + v );
363                        }
364                    }
365                    else
366                    {
367                        ChangeFile f =
368                            new ChangeFile( v.getElementName(), v.getVirtualSpec() + " (" + v.getRealSpec() + ")" );
369                        files.add( f );
370                    }
371    
372                }
373    
374                if ( versions.isEmpty() || !files.isEmpty() )
375                {
376                    ChangeSet changeSet = new ChangeSet( t.getWhen(), t.getComment(), t.getAuthor(), files );
377    
378                    entries.add( changeSet );
379                }
380                else
381                {
382                    if ( getLogger().isDebugEnabled() )
383                    {
384                        getLogger().debug( "All versions removed for " + t );
385                    }
386                }
387    
388            }
389    
390            // Anything left in the differencesMap represents a change from a higher stream
391            // We don't have details on who or where these came from, but it is important to
392            // detect these for CI tools like Continuum
393            if ( !differencesMap.isEmpty() )
394            {
395                List<ChangeFile> upstreamFiles = new ArrayList<ChangeFile>();
396                for ( FileDifference difference : differencesMap.values() )
397                {
398                    if ( difference.getNewVersionSpec() != null )
399                    {
400                        upstreamFiles.add( new ChangeFile( difference.getNewFile().getPath(),
401                                                           difference.getNewVersionSpec() ) );
402                    }
403                    else
404                    {
405                        // difference is a deletion
406                        upstreamFiles.add( new ChangeFile( difference.getOldFile().getPath(), null ) );
407                    }
408                }
409                entries.add( new ChangeSet( endDate, "Upstream changes", "various", upstreamFiles ) );
410            }
411    
412            return new ChangeLogSet( entries, startDate, endDate );
413        }
414    
415        public ChangeLogScmResult changelog( ScmProviderRepository repo, ScmFileSet testFileSet, CommandParameters params )
416            throws ScmException
417        {
418    
419            return (ChangeLogScmResult) execute( repo, testFileSet, params );
420        }
421    
422    }