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;
029import java.util.Objects;
030
031import org.apache.maven.scm.ChangeFile;
032import org.apache.maven.scm.ChangeSet;
033import org.apache.maven.scm.CommandParameter;
034import org.apache.maven.scm.CommandParameters;
035import org.apache.maven.scm.ScmBranch;
036import org.apache.maven.scm.ScmException;
037import org.apache.maven.scm.ScmFileSet;
038import org.apache.maven.scm.ScmResult;
039import org.apache.maven.scm.ScmRevision;
040import org.apache.maven.scm.ScmVersion;
041import org.apache.maven.scm.command.changelog.ChangeLogScmResult;
042import org.apache.maven.scm.command.changelog.ChangeLogSet;
043import org.apache.maven.scm.log.ScmLogger;
044import org.apache.maven.scm.provider.ScmProviderRepository;
045import org.apache.maven.scm.provider.accurev.AccuRev;
046import org.apache.maven.scm.provider.accurev.AccuRevCapability;
047import org.apache.maven.scm.provider.accurev.AccuRevException;
048import org.apache.maven.scm.provider.accurev.AccuRevScmProviderRepository;
049import org.apache.maven.scm.provider.accurev.AccuRevVersion;
050import org.apache.maven.scm.provider.accurev.FileDifference;
051import org.apache.maven.scm.provider.accurev.Stream;
052import org.apache.maven.scm.provider.accurev.Transaction;
053import org.apache.maven.scm.provider.accurev.Transaction.Version;
054import org.apache.maven.scm.provider.accurev.command.AbstractAccuRevCommand;
055import org.codehaus.plexus.util.StringUtils;
056
057/**
058 * TODO filter results based on project_path Find appropriate start and end transaction ids from parameters. Streams
059 * must be the same. Diff on stream start to end - these are the upstream changes Hist on the stream start+1 to end
060 * remove items from the upstream set if they appear in the history For workspaces diff doesn't work. So we would not
061 * pickup any upstream changes, just the "keep" transactions which is not very useful. Hist on the workspace Then diff /
062 * hist on the basis stream, skipping any transactions that are coming from the workspace.
063 * 
064 * @author ggardner
065 */
066public class AccuRevChangeLogCommand
067    extends AbstractAccuRevCommand
068{
069
070    public AccuRevChangeLogCommand( ScmLogger logger )
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 ( !Objects.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    private ChangeLogSet getChangeLog( Stream stream, List<FileDifference> streamDifferences,
292                                       List<Transaction> streamHistory, List<Transaction> workspaceHistory,
293                                       Date startDate, Date endDate )
294    {
295
296        // Collect all the "to" versions from the streamDifferences into a Map by element id
297        // If that version is seen in the promote/keep history then we move it from the map
298        // At the end we create a pseudo ChangeSet for any remaining entries in the map as
299        // representing "upstream changes"
300        Map<Long, FileDifference> differencesMap = new HashMap<Long, FileDifference>();
301        for ( FileDifference fileDifference : streamDifferences )
302        {
303            differencesMap.put( fileDifference.getElementId(), fileDifference );
304        }
305
306        List<Transaction> mergedHistory = new ArrayList<Transaction>( streamHistory );
307        // will never match a version
308        String streamPrefix = "/";
309
310        mergedHistory.addAll( workspaceHistory );
311        streamPrefix = stream.getId() + "/";
312
313        List<ChangeSet> entries = new ArrayList<ChangeSet>( streamHistory.size() );
314        for ( Transaction t : mergedHistory )
315        {
316            if ( ( startDate != null && t.getWhen().before( startDate ) )
317                || ( endDate != null && t.getWhen().after( endDate ) ) )
318            {
319                // This is possible if dates and transactions are mixed in the time spec.
320                continue;
321            }
322
323            // Needed to make Tck test pass against accurev > 4.7.2 - the changelog only expects to deal with
324            // files. Stream changes and cross links are important entries in the changelog.
325            // However we should only see mkstream once and it is irrelevant given we are interrogating
326            // the history of this stream.
327            if ( "mkstream".equals( t.getTranType() ) )
328            {
329                continue;
330            }
331
332            Collection<Version> versions = t.getVersions();
333            List<ChangeFile> files = new ArrayList<ChangeFile>( versions.size() );
334
335            for ( Version v : versions )
336            {
337
338                // Remove diff representing this promote
339                FileDifference difference = differencesMap.get( v.getElementId() );
340                // TODO: how are defuncts shown in the version history?
341                if ( difference != null )
342                {
343                    String newVersionSpec = difference.getNewVersionSpec();
344                    if ( newVersionSpec != null && newVersionSpec.equals( v.getRealSpec() ) )
345                    {
346                        if ( getLogger().isDebugEnabled() )
347                        {
348                            getLogger().debug( "Removing difference for " + v );
349                        }
350                        differencesMap.remove( v.getElementId() );
351                    }
352                }
353
354                // Add this file, unless the virtual version indicates this is the basis stream, and the real
355                // version came from our workspace stream (ie, this transaction is a promote from the workspace
356                // to its basis stream, and is therefore NOT a change
357                if ( v.getRealSpec().startsWith( streamPrefix ) && !v.getVirtualSpec().startsWith( streamPrefix ) )
358                {
359                    if ( getLogger().isDebugEnabled() )
360                    {
361                        getLogger().debug( "Skipping workspace to basis stream promote " + v );
362                    }
363                }
364                else
365                {
366                    ChangeFile f =
367                        new ChangeFile( v.getElementName(), v.getVirtualSpec() + " (" + v.getRealSpec() + ")" );
368                    files.add( f );
369                }
370
371            }
372
373            if ( versions.isEmpty() || !files.isEmpty() )
374            {
375                ChangeSet changeSet = new ChangeSet( t.getWhen(), t.getComment(), t.getAuthor(), files );
376
377                entries.add( changeSet );
378            }
379            else
380            {
381                if ( getLogger().isDebugEnabled() )
382                {
383                    getLogger().debug( "All versions removed for " + t );
384                }
385            }
386
387        }
388
389        // Anything left in the differencesMap represents a change from a higher stream
390        // We don't have details on who or where these came from, but it is important to
391        // detect these for CI tools like Continuum
392        if ( !differencesMap.isEmpty() )
393        {
394            List<ChangeFile> upstreamFiles = new ArrayList<ChangeFile>();
395            for ( FileDifference difference : differencesMap.values() )
396            {
397                if ( difference.getNewVersionSpec() != null )
398                {
399                    upstreamFiles.add( new ChangeFile( difference.getNewFile().getPath(),
400                                                       difference.getNewVersionSpec() ) );
401                }
402                else
403                {
404                    // difference is a deletion
405                    upstreamFiles.add( new ChangeFile( difference.getOldFile().getPath(), null ) );
406                }
407            }
408            entries.add( new ChangeSet( endDate, "Upstream changes", "various", upstreamFiles ) );
409        }
410
411        return new ChangeLogSet( entries, startDate, endDate );
412    }
413
414    public ChangeLogScmResult changelog( ScmProviderRepository repo, ScmFileSet testFileSet, CommandParameters params )
415        throws ScmException
416    {
417        return (ChangeLogScmResult) execute( repo, testFileSet, params );
418    }
419
420}