001package org.apache.maven.wagon.providers.scm; 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 org.apache.maven.scm.ScmBranch; 023import org.apache.maven.scm.ScmException; 024import org.apache.maven.scm.ScmFile; 025import org.apache.maven.scm.ScmFileSet; 026import org.apache.maven.scm.ScmResult; 027import org.apache.maven.scm.ScmRevision; 028import org.apache.maven.scm.ScmTag; 029import org.apache.maven.scm.ScmVersion; 030import org.apache.maven.scm.command.add.AddScmResult; 031import org.apache.maven.scm.command.checkout.CheckOutScmResult; 032import org.apache.maven.scm.command.list.ListScmResult; 033import org.apache.maven.scm.manager.NoSuchScmProviderException; 034import org.apache.maven.scm.manager.ScmManager; 035import org.apache.maven.scm.provider.ScmProvider; 036import org.apache.maven.scm.provider.ScmProviderRepository; 037import org.apache.maven.scm.provider.ScmProviderRepositoryWithHost; 038import org.apache.maven.scm.repository.ScmRepository; 039import org.apache.maven.scm.repository.ScmRepositoryException; 040import org.apache.maven.wagon.AbstractWagon; 041import org.apache.maven.wagon.ConnectionException; 042import org.apache.maven.wagon.ResourceDoesNotExistException; 043import org.apache.maven.wagon.TransferFailedException; 044import org.apache.maven.wagon.authorization.AuthorizationException; 045import org.apache.maven.wagon.events.TransferEvent; 046import org.apache.maven.wagon.resource.Resource; 047import org.codehaus.plexus.util.FileUtils; 048import org.codehaus.plexus.util.StringUtils; 049 050import java.io.File; 051import java.io.IOException; 052import java.text.DecimalFormat; 053import java.util.ArrayList; 054import java.util.List; 055import java.util.Random; 056import java.util.Stack; 057 058/** 059 * Wagon provider to get and put files from and to SCM systems, using Maven-SCM as underlying transport. 060 * <p/> 061 * TODO it probably creates problems if the same wagon is used in two different SCM protocols, as instance variables can 062 * keep incorrect state. 063 * TODO: For doing releases, we either have to be able to add files with checking out the repository structure which may not be 064 * possible, or the checkout directory needs to be a constant. Doing releases won't scale if you have to checkout the 065 * whole repository structure in order to add 3 files. 066 * 067 * @author <a href="brett@apache.org">Brett Porter</a> 068 * @author <a href="evenisse@apache.org">Emmanuel Venisse</a> 069 * @author <a href="carlos@apache.org">Carlos Sanchez</a> 070 * @author Jason van Zyl 071 * 072 * @plexus.component role="org.apache.maven.wagon.Wagon" role-hint="scm" instantiation-strategy="per-lookup" 073 */ 074public class ScmWagon 075 extends AbstractWagon 076{ 077 /** 078 * @plexus.requirement 079 */ 080 private volatile ScmManager scmManager; 081 082 /** 083 * The SCM version, if any. 084 * 085 * @parameter 086 */ 087 private String scmVersion; 088 089 /** 090 * The SCM version type, if any. Defaults to "branch". 091 * 092 * @parameter 093 */ 094 private String scmVersionType; 095 096 private File checkoutDirectory; 097 098 /** 099 * Get the {@link ScmManager} used in this Wagon 100 * 101 * @return the {@link ScmManager} 102 */ 103 public ScmManager getScmManager() 104 { 105 return scmManager; 106 } 107 108 /** 109 * Set the {@link ScmManager} used in this Wagon 110 * 111 * @param scmManager 112 */ 113 public void setScmManager( ScmManager scmManager ) 114 { 115 this.scmManager = scmManager; 116 } 117 118 /** 119 * Get the scmVersion used in this Wagon 120 * 121 * @return the scmVersion 122 */ 123 public String getScmVersion() 124 { 125 return scmVersion; 126 } 127 128 /** 129 * Set the scmVersion 130 * 131 * @param scmVersion the scmVersion to set 132 */ 133 public void setScmVersion( String scmVersion ) 134 { 135 this.scmVersion = scmVersion; 136 } 137 138 /** 139 * Get the scmVersionType used in this Wagon 140 * 141 * @return the scmVersionType 142 */ 143 public String getScmVersionType() 144 { 145 return scmVersionType; 146 } 147 148 /** 149 * Set the scmVersionType 150 * 151 * @param scmVersionType the scmVersionType to set 152 */ 153 public void setScmVersionType( String scmVersionType ) 154 { 155 this.scmVersionType = scmVersionType; 156 } 157 158 /** 159 * Get the directory where Wagon will checkout files from SCM. This directory will be deleted! 160 * 161 * @return directory 162 */ 163 public File getCheckoutDirectory() 164 { 165 return checkoutDirectory; 166 } 167 168 /** 169 * Set the directory where Wagon will checkout files from SCM. This directory will be deleted! 170 * 171 * @param checkoutDirectory 172 */ 173 public void setCheckoutDirectory( File checkoutDirectory ) 174 { 175 this.checkoutDirectory = checkoutDirectory; 176 } 177 178 /** 179 * Convenience method to get the {@link ScmProvider} implementation to handle the provided SCM type 180 * 181 * @param scmType type of SCM, eg. <code>svn</code>, <code>cvs</code> 182 * @return the {@link ScmProvider} that will handle provided SCM type 183 * @throws NoSuchScmProviderException if there is no {@link ScmProvider} able to handle that SCM type 184 */ 185 public ScmProvider getScmProvider( String scmType ) 186 throws NoSuchScmProviderException 187 { 188 return getScmManager().getProviderByType( scmType ); 189 } 190 191 /** 192 * This will cleanup the checkout directory 193 */ 194 public void openConnectionInternal() 195 throws ConnectionException 196 { 197 if ( checkoutDirectory == null ) 198 { 199 checkoutDirectory = createCheckoutDirectory(); 200 } 201 202 if ( checkoutDirectory.exists() ) 203 { 204 removeCheckoutDirectory(); 205 } 206 207 checkoutDirectory.mkdirs(); 208 } 209 210 private File createCheckoutDirectory() 211 { 212 File checkoutDirectory; 213 214 DecimalFormat fmt = new DecimalFormat( "#####" ); 215 216 Random rand = new Random( System.currentTimeMillis() + Runtime.getRuntime().freeMemory() ); 217 218 synchronized ( rand ) 219 { 220 do 221 { 222 checkoutDirectory = new File( System.getProperty( "java.io.tmpdir" ), 223 "wagon-scm" + fmt.format( Math.abs( rand.nextInt() ) ) + ".checkout" ); 224 } 225 while ( checkoutDirectory.exists() ); 226 } 227 228 return checkoutDirectory; 229 } 230 231 232 private void removeCheckoutDirectory() 233 throws ConnectionException 234 { 235 if ( checkoutDirectory == null ) 236 { 237 return; // Silently return. 238 } 239 240 try 241 { 242 FileUtils.deleteDirectory( checkoutDirectory ); 243 } 244 catch ( IOException e ) 245 { 246 throw new ConnectionException( "Unable to cleanup checkout directory", e ); 247 } 248 } 249 250 /** 251 * Construct the ScmVersion to use for operations. 252 * <p/> 253 * <p>If scmVersion is supplied, scmVersionType must also be supplied to 254 * take effect.</p> 255 */ 256 private ScmVersion makeScmVersion() 257 { 258 if ( StringUtils.isBlank( scmVersion ) ) 259 { 260 return null; 261 } 262 if ( scmVersion.length() > 0 ) 263 { 264 if ( "revision".equals( scmVersionType ) ) 265 { 266 return new ScmRevision( scmVersion ); 267 } 268 else if ( "tag".equals( scmVersionType ) ) 269 { 270 return new ScmTag( scmVersion ); 271 } 272 else if ( "branch".equals( scmVersionType ) ) 273 { 274 return new ScmBranch( scmVersion ); 275 } 276 } 277 278 return null; 279 } 280 281 private ScmRepository getScmRepository( String url ) 282 throws ScmRepositoryException, NoSuchScmProviderException 283 { 284 String username = null; 285 286 String password = null; 287 288 String privateKey = null; 289 290 String passphrase = null; 291 292 if ( authenticationInfo != null ) 293 { 294 username = authenticationInfo.getUserName(); 295 296 password = authenticationInfo.getPassword(); 297 298 privateKey = authenticationInfo.getPrivateKey(); 299 300 passphrase = authenticationInfo.getPassphrase(); 301 } 302 303 ScmRepository scmRepository = getScmManager().makeScmRepository( url ); 304 305 ScmProviderRepository providerRepository = scmRepository.getProviderRepository(); 306 307 if ( StringUtils.isNotEmpty( username ) ) 308 { 309 providerRepository.setUser( username ); 310 } 311 312 if ( StringUtils.isNotEmpty( password ) ) 313 { 314 providerRepository.setPassword( password ); 315 } 316 317 if ( providerRepository instanceof ScmProviderRepositoryWithHost ) 318 { 319 ScmProviderRepositoryWithHost providerRepo = (ScmProviderRepositoryWithHost) providerRepository; 320 321 if ( StringUtils.isNotEmpty( privateKey ) ) 322 { 323 providerRepo.setPrivateKey( privateKey ); 324 } 325 326 if ( StringUtils.isNotEmpty( passphrase ) ) 327 { 328 providerRepo.setPassphrase( passphrase ); 329 } 330 } 331 332 return scmRepository; 333 } 334 335 public void put( File source, String targetName ) 336 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 337 { 338 if ( source.isDirectory() ) 339 { 340 throw new IllegalArgumentException( "Source is a directory: " + source ); 341 } 342 putInternal( source, targetName ); 343 } 344 345 /** 346 * Puts both files and directories 347 * 348 * @param source 349 * @param targetName 350 * @throws TransferFailedException 351 */ 352 private void putInternal( File source, String targetName ) 353 throws TransferFailedException 354 { 355 Resource target = new Resource( targetName ); 356 357 firePutInitiated( target, source ); 358 359 try 360 { 361 ScmRepository scmRepository = getScmRepository( getRepository().getUrl() ); 362 363 target.setContentLength( source.length() ); 364 target.setLastModified( source.lastModified() ); 365 366 firePutStarted( target, source ); 367 368 String msg = "Wagon: Adding " + source.getName() + " to repository"; 369 370 ScmProvider scmProvider = getScmProvider( scmRepository.getProvider() ); 371 372 String checkoutTargetName = source.isDirectory() ? targetName : getDirname( targetName ); 373 String relPath = checkOut( scmProvider, scmRepository, checkoutTargetName, target ); 374 375 File newCheckoutDirectory = new File( checkoutDirectory, relPath ); 376 377 File scmFile = new File( newCheckoutDirectory, source.isDirectory() ? "" : getFilename( targetName ) ); 378 379 boolean fileAlreadyInScm = scmFile.exists(); 380 381 if ( !scmFile.equals( source ) ) 382 { 383 if ( source.isDirectory() ) 384 { 385 FileUtils.copyDirectoryStructure( source, scmFile ); 386 } 387 else 388 { 389 FileUtils.copyFile( source, scmFile ); 390 } 391 } 392 393 if ( !fileAlreadyInScm || scmFile.isDirectory() ) 394 { 395 int addedFiles = addFiles( scmProvider, scmRepository, newCheckoutDirectory, 396 source.isDirectory() ? "" : scmFile.getName() ); 397 398 if ( !fileAlreadyInScm && addedFiles == 0 ) 399 { 400 throw new ScmException( 401 "Unable to add file to SCM: " + scmFile + "; see error messages above for more information" ); 402 } 403 } 404 405 ScmResult result = 406 scmProvider.checkIn( scmRepository, new ScmFileSet( checkoutDirectory ), makeScmVersion(), msg ); 407 408 checkScmResult( result ); 409 } 410 catch ( ScmException e ) 411 { 412 fireTransferError( target, e, TransferEvent.REQUEST_GET ); 413 414 throw new TransferFailedException( "Error interacting with SCM: " + e.getMessage(), e ); 415 } 416 catch ( IOException e ) 417 { 418 fireTransferError( target, e, TransferEvent.REQUEST_GET ); 419 420 throw new TransferFailedException( "Error interacting with SCM: " + e.getMessage(), e ); 421 } 422 423 if ( source.isFile() ) 424 { 425 postProcessListeners( target, source, TransferEvent.REQUEST_PUT ); 426 } 427 428 firePutCompleted( target, source ); 429 } 430 431 /** 432 * Returns the relative path to targetName in the checkout dir. If the targetName already exists in the scm, this 433 * will be the empty string. 434 * 435 * @param scmProvider 436 * @param scmRepository 437 * @param targetName 438 * @return 439 * @throws TransferFailedException 440 */ 441 private String checkOut( ScmProvider scmProvider, ScmRepository scmRepository, String targetName, 442 Resource resource ) 443 throws TransferFailedException 444 { 445 checkoutDirectory = createCheckoutDirectory(); 446 447 Stack<String> stack = new Stack<String>(); 448 449 String target = targetName; 450 451 // totally ignore scmRepository parent stuff since that is not supported by all scms. 452 // Instead, assume that that url exists. If not, then that's an error. 453 // Check whether targetName, which is a relative path into the scm, exists. 454 // If it doesn't, check the parent, etc. 455 456 try 457 { 458 while ( target.length() > 0 && !scmProvider.list( scmRepository, 459 new ScmFileSet( new File( "." ), new File( target ) ), 460 false, makeScmVersion() ).isSuccess() ) 461 { 462 stack.push( getFilename( target ) ); 463 target = getDirname( target ); 464 } 465 } 466 catch ( ScmException e ) 467 { 468 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 469 470 throw new TransferFailedException( "Error listing repository: " + e.getMessage(), e ); 471 } 472 473 // ok, we've established that target exists, or is empty. 474 // Check the resource out; if it doesn't exist, that means we're in the svn repo url root, 475 // and the configuration is incorrect. We will not try repo.getParent since most scm's don't 476 // implement that. 477 478 try 479 { 480 String repoUrl = getRepository().getUrl(); 481 if ( "svn".equals( scmProvider.getScmType() ) ) 482 { 483 // Subversion is the only SCM that adds path structure to represent tags and branches. 484 // The rest use scmVersion and scmVersionType. 485 repoUrl += "/" + target.replace( '\\', '/' ); 486 } 487 scmRepository = getScmRepository( repoUrl ); 488 CheckOutScmResult ret = 489 scmProvider.checkOut( scmRepository, new ScmFileSet( new File( checkoutDirectory, "" ) ), 490 makeScmVersion(), false ); 491 492 checkScmResult( ret ); 493 } 494 catch ( ScmException e ) 495 { 496 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 497 498 throw new TransferFailedException( "Error checking out: " + e.getMessage(), e ); 499 } 500 501 // now create the subdirs in target, if it's a parent of targetName 502 503 String relPath = ""; 504 505 while ( !stack.isEmpty() ) 506 { 507 String p = stack.pop(); 508 relPath += p + "/"; 509 510 File newDir = new File( checkoutDirectory, relPath ); 511 if ( !newDir.mkdirs() ) 512 { 513 throw new TransferFailedException( 514 "Failed to create directory " + newDir.getAbsolutePath() + "; parent should exist: " 515 + checkoutDirectory ); 516 } 517 518 try 519 { 520 addFiles( scmProvider, scmRepository, checkoutDirectory, relPath ); 521 } 522 catch ( ScmException e ) 523 { 524 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 525 526 throw new TransferFailedException( "Failed to add directory " + newDir + " to working copy", e ); 527 } 528 } 529 530 return relPath; 531 } 532 533 /** 534 * Add a file or directory to a SCM repository. If it's a directory all its contents are added recursively. 535 * <p/> 536 * TODO this is less than optimal, SCM API should provide a way to add a directory recursively 537 * 538 * @param scmProvider SCM provider 539 * @param scmRepository SCM repository 540 * @param basedir local directory corresponding to scmRepository 541 * @param scmFilePath path of the file or directory to add, relative to basedir 542 * @return the number of files added. 543 * @throws ScmException 544 */ 545 private int addFiles( ScmProvider scmProvider, ScmRepository scmRepository, File basedir, String scmFilePath ) 546 throws ScmException 547 { 548 int addedFiles = 0; 549 550 File scmFile = new File( basedir, scmFilePath ); 551 552 if ( scmFilePath.length() != 0 ) 553 { 554 AddScmResult result = scmProvider.add( scmRepository, new ScmFileSet( basedir, new File( scmFilePath ) ) ); 555 556 /* 557 * TODO dirty fix to work around files with property svn:eol-style=native if a file has that property, first 558 * time file is added it fails, second time it succeeds the solution is check if the scm provider is svn and 559 * unset that property when the SCM API allows it 560 */ 561 if ( !result.isSuccess() ) 562 { 563 result = scmProvider.add( scmRepository, new ScmFileSet( basedir, new File( scmFilePath ) ) ); 564 } 565 566 addedFiles = result.getAddedFiles().size(); 567 } 568 569 String reservedScmFile = scmProvider.getScmSpecificFilename(); 570 571 if ( scmFile.isDirectory() ) 572 { 573 for ( File file : scmFile.listFiles() ) 574 { 575 if ( reservedScmFile != null && !reservedScmFile.equals( file.getName() ) ) 576 { 577 addedFiles += addFiles( scmProvider, scmRepository, basedir, 578 ( scmFilePath.length() == 0 ? "" : scmFilePath + "/" ) 579 + file.getName() ); 580 } 581 } 582 } 583 584 return addedFiles; 585 } 586 587 /** 588 * @return true 589 */ 590 public boolean supportsDirectoryCopy() 591 { 592 return true; 593 } 594 595 public void putDirectory( File sourceDirectory, String destinationDirectory ) 596 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 597 { 598 if ( !sourceDirectory.isDirectory() ) 599 { 600 throw new IllegalArgumentException( "Source is not a directory: " + sourceDirectory ); 601 } 602 603 putInternal( sourceDirectory, destinationDirectory ); 604 } 605 606 /** 607 * Check that the ScmResult was a successful operation 608 * 609 * @param result 610 * @throws TransferFailedException if result was not a successful operation 611 * @throws ScmException 612 */ 613 private void checkScmResult( ScmResult result ) 614 throws ScmException 615 { 616 if ( !result.isSuccess() ) 617 { 618 throw new ScmException( 619 "Unable to commit file. " + result.getProviderMessage() + " " + ( result.getCommandOutput() == null 620 ? "" 621 : result.getCommandOutput() ) ); 622 } 623 } 624 625 public void closeConnection() 626 throws ConnectionException 627 { 628 removeCheckoutDirectory(); 629 } 630 631 /** 632 * Not implemented 633 * 634 * @throws UnsupportedOperationException always 635 */ 636 public boolean getIfNewer( String resourceName, File destination, long timestamp ) 637 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 638 { 639 throw new UnsupportedOperationException( "Not currently supported: getIfNewer" ); 640 } 641 642 public void get( String resourceName, File destination ) 643 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 644 { 645 Resource resource = new Resource( resourceName ); 646 647 fireGetInitiated( resource, destination ); 648 649 String url = getRepository().getUrl() + "/" + resourceName; 650 651 // remove the file 652 url = url.substring( 0, url.lastIndexOf( '/' ) ); 653 654 try 655 { 656 ScmRepository scmRepository = getScmRepository( url ); 657 658 fireGetStarted( resource, destination ); 659 660 // TODO: limitations: 661 // - destination filename must match that in the repository - should allow the "-d" CVS equiv to be passed 662 // in 663 // - we don't get granular exceptions from SCM (ie, auth, not found) 664 // - need to make it non-recursive to save time 665 // - exists() check doesn't test if it is in SCM already 666 667 File scmFile = new File( checkoutDirectory, resourceName ); 668 669 File basedir = scmFile.getParentFile(); 670 671 ScmProvider scmProvider = getScmProvider( scmRepository.getProvider() ); 672 673 String reservedScmFile = scmProvider.getScmSpecificFilename(); 674 675 if ( reservedScmFile != null && new File( basedir, reservedScmFile ).exists() ) 676 { 677 scmProvider.update( scmRepository, new ScmFileSet( basedir ), makeScmVersion() ); 678 } 679 else 680 { 681 // TODO: this should be checking out a full hierarchy (requires the -d equiv) 682 basedir.mkdirs(); 683 684 scmProvider.checkOut( scmRepository, new ScmFileSet( basedir ), makeScmVersion() ); 685 } 686 687 if ( !scmFile.exists() ) 688 { 689 throw new ResourceDoesNotExistException( "Unable to find resource " + destination + " after checkout" ); 690 } 691 692 if ( !scmFile.equals( destination ) ) 693 { 694 FileUtils.copyFile( scmFile, destination ); 695 } 696 } 697 catch ( ScmException e ) 698 { 699 fireTransferError( resource, e, TransferEvent.REQUEST_GET ); 700 701 throw new TransferFailedException( "Error getting file from SCM", e ); 702 } 703 catch ( IOException e ) 704 { 705 fireTransferError( resource, e, TransferEvent.REQUEST_GET ); 706 707 throw new TransferFailedException( "Error getting file from SCM", e ); 708 } 709 710 postProcessListeners( resource, destination, TransferEvent.REQUEST_GET ); 711 712 fireGetCompleted( resource, destination ); 713 } 714 715 /** 716 * @return a List<String> with filenames/directories at the resourcepath. 717 * @see org.apache.maven.wagon.AbstractWagon#getFileList(java.lang.String) 718 */ 719 public List<String> getFileList( String resourcePath ) 720 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 721 { 722 try 723 { 724 ScmRepository repository = getScmRepository( getRepository().getUrl() ); 725 726 ScmProvider provider = getScmProvider( repository.getProvider() ); 727 728 ListScmResult result = 729 provider.list( repository, new ScmFileSet( new File( "." ), new File( resourcePath ) ), false, 730 makeScmVersion() ); 731 732 if ( !result.isSuccess() ) 733 { 734 throw new ResourceDoesNotExistException( result.getProviderMessage() ); 735 } 736 737 List<String> files = new ArrayList<String>(); 738 739 for ( ScmFile f : result.getFiles() ) 740 { 741 files.add( f.getPath() ); 742 } 743 744 return files; 745 } 746 catch ( ScmException e ) 747 { 748 throw new TransferFailedException( "Error getting filelist from SCM", e ); 749 } 750 } 751 752 public boolean resourceExists( String resourceName ) 753 throws TransferFailedException, AuthorizationException 754 { 755 try 756 { 757 getFileList( resourceName ); 758 759 return true; 760 } 761 catch ( ResourceDoesNotExistException e ) 762 { 763 return false; 764 } 765 } 766 767 private String getFilename( String filename ) 768 { 769 String fname = StringUtils.replace( filename, "/", File.separator ); 770 return FileUtils.filename( fname ); 771 } 772 773 private String getDirname( String filename ) 774 { 775 String fname = StringUtils.replace( filename, "/", File.separator ); 776 return FileUtils.dirname( fname ); 777 } 778}