1 package hudson.scm; 2 3 import hudson.FilePath; 4 import hudson.FilePath.FileCallable; 5 import hudson.Launcher; 6 import hudson.Util; 7 import static hudson.Util.fixNull; 8 import hudson.model.AbstractBuild; 9 import hudson.model.AbstractProject; 10 import hudson.model.BuildListener; 11 import hudson.model.Descriptor; 12 import hudson.model.Hudson; 13 import hudson.model.TaskListener; 14 import hudson.remoting.Channel; 15 import hudson.remoting.VirtualChannel; 16 import hudson.util.FormFieldValidator; 17 import hudson.util.Scrambler; 18 import org.kohsuke.stapler.StaplerRequest; 19 import org.kohsuke.stapler.StaplerResponse; 20 import org.tmatesoft.svn.core.SVNErrorMessage; 21 import org.tmatesoft.svn.core.SVNException; 22 import org.tmatesoft.svn.core.SVNURL; 23 import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; 24 import org.tmatesoft.svn.core.auth.ISVNAuthenticationProvider; 25 import org.tmatesoft.svn.core.auth.SVNAuthentication; 26 import org.tmatesoft.svn.core.auth.SVNPasswordAuthentication; 27 import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory; 28 import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory; 29 import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl; 30 import org.tmatesoft.svn.core.internal.wc.DefaultSVNAuthenticationManager; 31 import org.tmatesoft.svn.core.io.SVNRepository; 32 import org.tmatesoft.svn.core.io.SVNRepositoryFactory; 33 import org.tmatesoft.svn.core.wc.SVNClientManager; 34 import org.tmatesoft.svn.core.wc.SVNInfo; 35 import org.tmatesoft.svn.core.wc.SVNLogClient; 36 import org.tmatesoft.svn.core.wc.SVNRevision; 37 import org.tmatesoft.svn.core.wc.SVNUpdateClient; 38 import org.tmatesoft.svn.core.wc.SVNWCClient; 39 import org.tmatesoft.svn.core.wc.SVNWCUtil; 40 import org.tmatesoft.svn.core.wc.xml.SVNXMLLogHandler; 41 import org.xml.sax.helpers.LocatorImpl ; 42 43 import javax.servlet.ServletException ; 44 import javax.xml.transform.TransformerConfigurationException ; 45 import javax.xml.transform.sax.SAXTransformerFactory ; 46 import javax.xml.transform.sax.TransformerHandler ; 47 import javax.xml.transform.stream.StreamResult ; 48 import java.io.BufferedReader ; 49 import java.io.File ; 50 import java.io.FileOutputStream ; 51 import java.io.FileReader ; 52 import java.io.IOException ; 53 import java.io.PrintStream ; 54 import java.io.PrintWriter ; 55 import java.io.Serializable ; 56 import java.io.StringWriter ; 57 import java.util.ArrayList ; 58 import java.util.Collection ; 59 import java.util.HashMap ; 60 import java.util.Hashtable ; 61 import java.util.List ; 62 import java.util.Map ; 63 import java.util.Map.Entry; 64 import java.util.StringTokenizer ; 65 import java.util.logging.Logger ; 66 import java.util.logging.Level ; 67 68 76 public class SubversionSCM extends SCM implements Serializable { 77 private final String modules; 78 private boolean useUpdate; 79 private String username; 80 81 85 private transient String otherOptions; 86 87 SubversionSCM(String modules, boolean useUpdate, String username) { 88 StringBuilder normalizedModules = new StringBuilder (); 89 StringTokenizer tokens = new StringTokenizer (modules); 90 while(tokens.hasMoreTokens()) { 91 if(normalizedModules.length()>0) normalizedModules.append(' '); 92 String m = tokens.nextToken(); 93 if(m.endsWith("/")) 94 m = m.substring(0,m.length()-1); 96 normalizedModules.append(m); 97 } 98 99 this.modules = normalizedModules.toString(); 100 this.useUpdate = useUpdate; 101 this.username = nullify(username); 102 } 103 104 108 public String getModules() { 109 return modules; 110 } 111 112 public boolean isUseUpdate() { 113 return useUpdate; 114 } 115 116 public String getUsername() { 117 return username; 118 } 119 120 private Collection <String > getModuleDirNames() { 121 List <String > dirs = new ArrayList <String >(); 122 StringTokenizer tokens = new StringTokenizer (modules); 123 while(tokens.hasMoreTokens()) { 124 dirs.add(getLastPathComponent(tokens.nextToken())); 125 } 126 return dirs; 127 } 128 129 private boolean calcChangeLog(AbstractBuild<?, ?> build, File changelogFile, BuildListener listener) throws IOException { 130 if(build.getPreviousBuild()==null) { 131 return createEmptyChangeLog(changelogFile, listener, "log"); 133 } 134 135 PrintStream logger = listener.getLogger(); 136 137 Map <String ,Long > previousRevisions = parseRevisionFile(build.getPreviousBuild()); 138 Map <String ,Long > thisRevisions = parseRevisionFile(build); 139 140 boolean changelogFileCreated = false; 141 142 SVNLogClient svnlc = createSvnClientManager(getDescriptor().createAuthenticationProvider()).getLogClient(); 143 144 TransformerHandler th = createTransformerHandler(); 145 th.setResult(new StreamResult (changelogFile)); 146 SVNXMLLogHandler logHandler = new SVNXMLLogHandler(th); 147 th.setDocumentLocator(DUMMY_LOCATOR); 149 logHandler.startDocument(); 150 151 152 StringTokenizer tokens = new StringTokenizer (modules); 153 while(tokens.hasMoreTokens()) { 154 String url = tokens.nextToken(); 155 Long prevRev = previousRevisions.get(url); 156 if(prevRev==null) { 157 logger.println("no revision recorded for "+url+" in the previous build"); 158 continue; 159 } 160 Long thisRev = thisRevisions.get(url); 161 if(thisRev.equals(prevRev)) { 162 logger.println("no change for "+url+" since the previous build"); 163 continue; 164 } 165 166 try { 167 svnlc.doLog(SVNURL.parseURIEncoded(url),null, 168 SVNRevision.create(prevRev), SVNRevision.create(prevRev+1), 169 SVNRevision.create(thisRev), 170 false, true, Long.MAX_VALUE, logHandler); 171 } catch (SVNException e) { 172 e.printStackTrace(listener.error("revision check failed on "+url)); 173 } 174 changelogFileCreated = true; 175 } 176 177 if(changelogFileCreated) { 178 logHandler.endDocument(); 179 } 180 181 if(!changelogFileCreated) 182 createEmptyChangeLog(changelogFile, listener, "log"); 183 184 return true; 185 } 186 187 190 private static TransformerHandler createTransformerHandler() { 191 try { 192 return ((SAXTransformerFactory ) SAXTransformerFactory.newInstance()).newTransformerHandler(); 193 } catch (TransformerConfigurationException e) { 194 throw new Error (e); } 196 } 197 198 static Map <String ,Long > parseRevisionFile(AbstractBuild build) throws IOException { 199 Map <String ,Long > revisions = new HashMap <String ,Long >(); { File file = getRevisionFile(build); 202 if(!file.exists()) 203 return revisions; 205 206 BufferedReader br = new BufferedReader (new FileReader (file)); 207 String line; 208 while((line=br.readLine())!=null) { 209 int index = line.lastIndexOf('/'); 210 if(index<0) { 211 continue; } 213 try { 214 revisions.put(line.substring(0,index), Long.parseLong(line.substring(index+1))); 215 } catch (NumberFormatException e) { 216 } 218 } 219 } 220 221 return revisions; 222 } 223 224 public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspace, final BuildListener listener, File changelogFile) throws IOException , InterruptedException { 225 if(!checkout(launcher,workspace, listener)) 226 return false; 227 228 PrintWriter w = new PrintWriter (new FileOutputStream (getRevisionFile(build))); 230 try { 231 Map <String ,SvnInfo> revMap = buildRevisionMap(workspace, listener); 232 for (Entry<String ,SvnInfo> e : revMap.entrySet()) { 233 w.println( e.getKey() +'/'+ e.getValue().revision ); 234 } 235 } finally { 236 w.close(); 237 } 238 239 return calcChangeLog(build, changelogFile, listener); 240 } 241 242 public boolean checkout(Launcher launcher, FilePath workspace, final TaskListener listener) throws IOException , InterruptedException { 243 if(useUpdate && isUpdatable(workspace, listener)) { 244 return update(launcher,workspace,listener); 245 } else { 246 final ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(); 247 return workspace.act(new FileCallable<Boolean >() { 248 public Boolean invoke(File ws, VirtualChannel channel) throws IOException { 249 Util.deleteContentsRecursive(ws); 250 SVNUpdateClient svnuc = createSvnClientManager(authProvider).getUpdateClient(); 251 svnuc.setEventHandler(new SubversionUpdateEventHandler(listener)); 252 253 StringTokenizer tokens = new StringTokenizer (modules); 254 while(tokens.hasMoreTokens()) { 255 try { 256 SVNURL url = SVNURL.parseURIEncoded(tokens.nextToken()); 257 listener.getLogger().println("Checking out "+url); 258 259 svnuc.doCheckout(url, new File (ws, getLastPathComponent(url.getPath())), SVNRevision.HEAD, SVNRevision.HEAD, true ); 260 } catch (SVNException e) { 261 e.printStackTrace(listener.error("Error in subversion")); 262 return false; 263 } 264 } 265 266 return true; 267 } 268 }); 269 } 270 } 271 272 283 private static SVNClientManager createSvnClientManager(ISVNAuthenticationProvider authProvider) { 284 ISVNAuthenticationManager sam = SVNWCUtil.createDefaultAuthenticationManager(); 285 sam.setAuthenticationProvider(authProvider); 286 return SVNClientManager.newInstance(SVNWCUtil.createDefaultOptions(true),sam); 287 } 288 289 public static final class SvnInfo implements Serializable { 290 293 final String url; 294 final long revision; 295 296 public SvnInfo(String url, long revision) { 297 this.url = url; 298 this.revision = revision; 299 } 300 301 public SvnInfo(SVNInfo info) { 302 this( info.getURL().toDecodedString(), info.getCommittedRevision().getNumber() ); 303 } 304 305 public SVNURL getSVNURL() throws SVNException { 306 return SVNURL.parseURIDecoded(url); 307 } 308 309 private static final long serialVersionUID = 1L; 310 } 311 312 318 private SVNInfo parseSvnInfo(File workspace, ISVNAuthenticationProvider authProvider) throws SVNException { 319 SVNWCClient svnWc = createSvnClientManager(authProvider).getWCClient(); 320 return svnWc.doInfo(workspace,SVNRevision.WORKING); 321 } 322 323 329 private SVNInfo parseSvnInfo(SVNURL remoteUrl, ISVNAuthenticationProvider authProvider) throws SVNException { 330 SVNWCClient svnWc = createSvnClientManager(authProvider).getWCClient(); 331 return svnWc.doInfo(remoteUrl, SVNRevision.HEAD, SVNRevision.HEAD); 332 } 333 334 341 private Map <String ,SvnInfo> buildRevisionMap(FilePath workspace, final TaskListener listener) throws IOException , InterruptedException { 342 final ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(); 343 return workspace.act(new FileCallable<Map <String ,SvnInfo>>() { 344 public Map <String ,SvnInfo> invoke(File ws, VirtualChannel channel) throws IOException { 345 Map <String ,SvnInfo> revisions = new HashMap <String ,SvnInfo>(); 346 347 SVNWCClient svnWc = createSvnClientManager(authProvider).getWCClient(); 348 for( String module : getModuleDirNames() ) { 350 try { 351 SvnInfo info = new SvnInfo(svnWc.doInfo(new File (ws,module),SVNRevision.WORKING)); 352 revisions.put(info.url,info); 353 } catch (SVNException e) { 354 e.printStackTrace(listener.error("Failed to parse svn info for "+module)); 355 } 356 } 357 358 return revisions; 359 } 360 }); 361 } 362 363 366 private static File getRevisionFile(AbstractBuild build) { 367 return new File (build.getRootDir(),"revision.txt"); 368 } 369 370 public boolean update(Launcher launcher, FilePath workspace, final TaskListener listener) throws IOException , InterruptedException { 371 final ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(); 372 return workspace.act(new FileCallable<Boolean >() { 373 public Boolean invoke(File ws, VirtualChannel channel) throws IOException { 374 SVNUpdateClient svnuc = createSvnClientManager(authProvider).getUpdateClient(); 375 svnuc.setEventHandler(new SubversionUpdateEventHandler(listener)); 376 377 StringTokenizer tokens = new StringTokenizer (modules); 378 while(tokens.hasMoreTokens()) { 379 try { 380 String url = tokens.nextToken(); 381 listener.getLogger().println("Updating "+url); 382 svnuc.doUpdate(new File (ws, getLastPathComponent(url)), SVNRevision.HEAD, true ); 383 } catch (SVNException e) { 384 e.printStackTrace(listener.error("Error in subversion")); 385 return false; 386 } 387 } 388 return true; 389 } 390 }); 391 } 392 393 396 private boolean isUpdatable(FilePath workspace, final TaskListener listener) throws IOException , InterruptedException { 397 final ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(); 398 399 return workspace.act(new FileCallable<Boolean >() { 400 public Boolean invoke(File ws, VirtualChannel channel) throws IOException { 401 StringTokenizer tokens = new StringTokenizer (modules); 402 while(tokens.hasMoreTokens()) { 403 String url = tokens.nextToken(); 404 String moduleName = getLastPathComponent(url); 405 File module = new File (ws,moduleName); 406 407 if(!module.exists()) { 408 listener.getLogger().println("Checking out a fresh workspace because "+module+" doesn't exist"); 409 return false; 410 } 411 412 try { 413 SvnInfo svnInfo = new SvnInfo(parseSvnInfo(module,authProvider)); 414 if(!svnInfo.url.equals(url)) { 415 listener.getLogger().println("Checking out a fresh workspace because the workspace is not "+url); 416 return false; 417 } 418 } catch (SVNException e) { 419 listener.getLogger().println("Checking out a fresh workspace because Hudson failed to detect the current workspace "+module); 420 e.printStackTrace(listener.error(e.getMessage())); 421 return false; 422 } 423 } 424 return true; 425 } 426 }); 427 } 428 429 public boolean pollChanges(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException , InterruptedException { 430 Map <String ,SvnInfo> wsRev = buildRevisionMap(workspace, listener); 432 433 ISVNAuthenticationProvider authProvider = getDescriptor().createAuthenticationProvider(); 434 435 for (SvnInfo localInfo : wsRev.values()) { 437 try { 438 SvnInfo remoteInfo = new SvnInfo(parseSvnInfo(localInfo.getSVNURL(),authProvider)); 439 listener.getLogger().println("Revision:"+remoteInfo.revision); 440 if(remoteInfo.revision > localInfo.revision) 441 return true; } catch (SVNException e) { 443 e.printStackTrace(listener.error("Failed to check repository revision for "+localInfo.url)); 444 } 445 } 446 447 return false; } 449 450 public ChangeLogParser createChangeLogParser() { 451 return new SubversionChangeLogParser(); 452 } 453 454 455 public DescriptorImpl getDescriptor() { 456 return DescriptorImpl.DESCRIPTOR; 457 } 458 459 public void buildEnvVars(Map <String ,String > env) { 460 } 462 463 public FilePath getModuleRoot(FilePath workspace) { 464 String s; 465 466 int idx = modules.indexOf(' '); 468 if(idx>=0) s = modules.substring(0,idx); 469 else s = modules; 470 471 return workspace.child(getLastPathComponent(s)); 472 } 473 474 private static String getLastPathComponent(String s) { 475 String [] tokens = s.split("/"); 476 return tokens[tokens.length-1]; } 478 479 public static final class DescriptorImpl extends Descriptor<SCM> { 480 public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); 481 482 488 private volatile String svnExe; 489 490 493 private final Map <String ,Credential> credentials = new Hashtable <String ,Credential>(); 494 495 498 private static abstract class Credential implements Serializable { 499 abstract SVNAuthentication createSVNAuthentication(); 500 } 501 502 private static final class PasswordCredential extends Credential { 503 private final String userName; 504 private final String password; 506 public PasswordCredential(String userName, String password) { 507 this.userName = userName; 508 this.password = Scrambler.scramble(password); 509 } 510 511 @Override 512 SVNPasswordAuthentication createSVNAuthentication() { 513 return new SVNPasswordAuthentication(userName,Scrambler.descramble(password),false); 514 } 515 } 516 517 521 private interface RemotableSVNAuthenticationProvider { 522 Credential getCredential(String realm); 523 } 524 525 private final class RemotableSVNAuthenticationProviderImpl implements RemotableSVNAuthenticationProvider, Serializable { 526 public Credential getCredential(String realm) { 527 return credentials.get(realm); 528 } 529 530 533 private Object writeReplace() { 534 return Channel.current().export(RemotableSVNAuthenticationProvider.class, this); 535 } 536 } 537 538 541 private static final class SVNAuthenticationProviderImpl implements ISVNAuthenticationProvider, Serializable { 542 private final RemotableSVNAuthenticationProvider source; 543 544 public SVNAuthenticationProviderImpl(RemotableSVNAuthenticationProvider source) { 545 this.source = source; 546 } 547 548 public SVNAuthentication requestClientAuthentication(String kind, SVNURL url, String realm, SVNErrorMessage errorMessage, SVNAuthentication previousAuth, boolean authMayBeStored) { 549 Credential cred = source.getCredential(realm); 550 if(cred==null) return null; 551 return cred.createSVNAuthentication(); 552 } 553 554 public int acceptServerAuthentication(SVNURL url, String realm, Object certificate, boolean resultMayBeStored) { 555 return ACCEPTED_TEMPORARY; 556 } 557 558 private static final long serialVersionUID = 1L; 559 } 560 561 private DescriptorImpl() { 562 super(SubversionSCM.class); 563 load(); 564 } 565 566 public String getDisplayName() { 567 return "Subversion"; 568 } 569 570 public SCM newInstance(StaplerRequest req) { 571 return new SubversionSCM( 572 req.getParameter("svn_modules"), 573 req.getParameter("svn_use_update")!=null, 574 req.getParameter("svn_username") 575 ); 576 } 577 578 582 public ISVNAuthenticationProvider createAuthenticationProvider() { 583 return new SVNAuthenticationProviderImpl(new RemotableSVNAuthenticationProviderImpl()); 584 } 585 586 590 public void doAuthenticationCheck(final StaplerRequest req, StaplerResponse rsp) throws IOException , ServletException { 591 new FormFieldValidator(req,rsp,true) { 592 protected void check() throws IOException , ServletException { 593 StringTokenizer tokens = new StringTokenizer (fixNull(request.getParameter("value"))); 594 String message=""; 595 596 while(tokens.hasMoreTokens()) { 597 String url = tokens.nextToken(); 598 599 try { 600 SVNRepository repository = SVNRepositoryFactory.create(SVNURL.parseURIDecoded(url)); 601 602 ISVNAuthenticationManager sam = SVNWCUtil.createDefaultAuthenticationManager(); 603 sam.setAuthenticationProvider(createAuthenticationProvider()); 604 repository.setAuthenticationManager(sam); 605 606 repository.testConnection(); 607 } catch (SVNException e) { 608 StringWriter sw = new StringWriter (); 609 e.printStackTrace(new PrintWriter (sw)); 610 611 message += "Unable to access "+url+" : "+Util.escape( e.getErrorMessage().getFullMessage()); 612 message += " <a HREF='#' id=svnerrorlink onclick='javascript:" + 613 "document.getElementById(\"svnerror\").style.display=\"block\";" + 614 "document.getElementById(\"svnerrorlink\").style.display=\"none\";" + 615 "return false;'>(show details)</a>"; 616 message += "<pre id=svnerror style='display:none'>"+sw+"</pre>"; 617 message += " (Maybe you need to <a HREF='"+req.getContextPath()+"/scm/SubversionSCM/enterCredential?"+url+"'>enter credential</a>?)"; 618 message += "<br>"; 619 logger.log(Level.INFO, "Failed to access subversion repository "+url,e); 620 } 621 } 622 623 if(message.length()==0) 624 ok(); 625 else 626 error(message); 627 } 628 }.process(); 629 } 630 631 634 public void doPostCredential(final StaplerRequest req, StaplerResponse rsp) throws IOException , ServletException { 635 final String url = req.getParameter("url"); 636 final String username = req.getParameter("username"); 637 final String password = req.getParameter("password"); 638 639 try { 640 SVNRepository repository = SVNRepositoryFactory.create(SVNURL.parseURIDecoded(url)); 647 repository.setAuthenticationManager(new DefaultSVNAuthenticationManager(SVNWCUtil.getDefaultConfigurationDirectory(),true,username,password) { 648 public void acknowledgeAuthentication(boolean accepted, String kind, String realm, SVNErrorMessage errorMessage, SVNAuthentication authentication) throws SVNException { 649 if(accepted) { 650 credentials.put(realm,new PasswordCredential(username,password)); 651 save(); 652 } 653 super.acknowledgeAuthentication(accepted, kind, realm, errorMessage, authentication); 654 } 655 }); 656 repository.testConnection(); 657 rsp.sendRedirect("credentialOK"); 658 } catch (SVNException e) { 659 req.setAttribute("message",e.getErrorMessage()); 660 rsp.forward(Hudson.getInstance(),"error",req); 661 } 662 } 663 664 static { new Initializer(); } 665 } 666 667 private static final long serialVersionUID = 1L; 668 669 private static final Logger logger = Logger.getLogger(SubversionSCM.class.getName()); 670 671 private static final LocatorImpl DUMMY_LOCATOR = new LocatorImpl (); 672 673 static { 674 new Initializer(); 675 DUMMY_LOCATOR.setLineNumber(-1); 676 DUMMY_LOCATOR.setColumnNumber(-1); 677 } 678 679 private static final class Initializer { 680 static { 681 DAVRepositoryFactory.setup(); SVNRepositoryFactoryImpl.setup(); FSRepositoryFactory.setup(); } 685 } 686 } 687 | Popular Tags |