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