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}