1/* 2 * Copyright 2004-2005 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import org.tmatesoft.svn.core.io.SVNRepositoryFactory 18import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory 19import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory 20import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl 21import org.tmatesoft.svn.core.io.* 22import org.tmatesoft.svn.core.* 23import org.tmatesoft.svn.core.auth.* 24import org.tmatesoft.svn.core.wc.* 25import org.codehaus.groovy.grails.documentation.MetadataGeneratingMetaClassCreationHandle 26import org.codehaus.groovy.grails.plugins.publishing.DefaultPluginPublisher 27import org.springframework.core.io.FileSystemResource 28import groovy.xml.DOMBuilder 29import groovy.xml.dom.DOMCategory 30import javax.xml.transform.TransformerFactory 31import javax.xml.transform.OutputKeys 32import javax.xml.transform.Transformer 33import javax.xml.transform.dom.DOMSource 34import javax.xml.transform.stream.StreamResult 35import org.codehaus.groovy.grails.plugins.PluginManagerHolder 36 37/** 38 * Gant script that handles releasing plugins to a plugin repository. 39 * 40 * @author Graeme Rocher 41 */ 42 43includeTargets << grailsScript("_GrailsPluginDev") 44includeTargets << grailsScript("_GrailsBootstrap") 45includeTargets << grailsScript("_GrailsDocs") 46 47authManager = null 48commitMessage = null 49trunk = null 50latestRelease = null 51versionedRelease = null 52skipLatest = false 53 54CORE_PLUGIN_DIST = "http://svn.codehaus.org/grails/trunk/grails-plugins" 55CORE_PUBLISH_URL = "https://svn.codehaus.org/grails/trunk/grails-plugins" 56DEFAULT_PLUGIN_DIST = "http://plugins.grails.org" 57DEFAULT_PUBLISH_URL = "https://svn.codehaus.org/grails-plugins" 58 59// setup default plugin respositories for discovery 60pluginDiscoveryRepositories = [core:CORE_PLUGIN_DIST, default:DEFAULT_PLUGIN_DIST] 61if (grailsSettings?.config?.grails?.plugin?.repos?.discovery) { 62 pluginDiscoveryRepositories.putAll(grailsSettings?.config?.grails?.plugin?.repos?.discovery) 63} 64 65// setup default plugin respositories for publishing 66pluginDistributionRepositories = [core:CORE_PUBLISH_URL, default:DEFAULT_PUBLISH_URL] 67if (grailsSettings?.config?.grails?.plugin?.repos?.distribution) { 68 pluginDistributionRepositories.putAll(grailsSettings?.config?.grails?.plugin?.repos?.distribution) 69} 70 71KEY_URL = "url" 72KEY_USER_NAME = "user" 73KEY_USER_PASS = "pswd" 74 75/** 76 * authentication manager Map 77 * EG : INIT CORE OR DEFAULT DISTRIBUTION REPOSITORY 78 * aMap = [url:CORE_PUBLISH_URL,user:"auser",pswd:"apswd"] 79 * ALLOWS ANT PROPERTIES DEFINITION 80 * aAuthManager = getAuthenticationManager("core", "distribution", aMap) 81 * authManagerMap.put("distribution.core", aAuthManager) 82 */ 83authManagerMap = [:] 84 85/** 86 * Break down url into separate authentication components from url 87 * @param url to be parsed 88 * @return broken down url in these parts (if exist) url, user and pswd 89 */ 90public Map tokenizeUrl(String url) throws SVNException { 91 SVNURL aURL= SVNURL.parseURIDecoded(url) 92 def aHashMap = [:] 93 94 def userInfo = aURL.userInfo 95 if (userInfo) { 96 def userInfoArray = userInfo.split(":") 97 aHashMap[KEY_USER_NAME] = userInfoArray[0] 98 if (userInfoArray.length>1) { 99 aHashMap[KEY_USER_PASS] = userInfoArray[1] 100 } 101 } 102 if (aURL.port == 443) { 103 aHashMap[KEY_URL] = "${aURL.protocol}://${aURL.host}${aURL.path}".toString() 104 } 105 else { 106 aHashMap[KEY_URL] = "${aURL.protocol}://${aURL.host}:${aURL.port}${aURL.path}".toString() 107 } 108 109 return aHashMap 110} 111 112//init SVN Kit one time 113// support file based SVN 114FSRepositoryFactory.setup() 115// support the server http/https 116DAVRepositoryFactory.setup() 117// support svn protocol 118SVNRepositoryFactoryImpl.setup() 119 120/** 121 * Replace the url with authentication by url without in discovery and distribution 122 * repository and setup authentication instance in authManagerMap. 123 * if an url like : 124 * {protocol}://{user:password}@url is defined the promt does not occur. 125 * Else the user is prompted for user and password values. 126 * The repos "core" and "default" are ignored (see above for default configuration) 127 * For all other repo, a defultAuthenticationManager is created. 128 */ 129public Map configureAuth(Map repoMap,String repoType) { 130 repoMapTmp = [:] 131 repoMap.each { 132 ISVNAuthenticationManager aAuthManager = SVNWCUtil.createDefaultAuthenticationManager() 133 if ("core" == it.key || "default" == it.key) { 134 repoMapTmp[it.key] = it.value 135 if (!isSecureUrl(it.value)) { 136 authManagerMap[repoType+"."+it.key] = aAuthManager 137 } 138 // else no authentication manager provides to authManagerMap : case of CORE_PUBLISH_URL and DEFAULT_PUBLISH_URL 139 } 140 else { 141 if (isSecureUrl(it.value)) { 142 event "StatusUpdate", ["Authentication for svn repo at ${it.key} ${repoType} is required."] 143 aMap = tokenizeUrl(it.value) 144 repoMapTmp[it.key] = aMap[KEY_URL] 145 aAuthManager = getAuthenticationManager(it.key, repoType, aMap) 146 authManagerMap[repoType + "." + it.key] = aAuthManager 147 } 148 else { 149 repoMapTmp[it.key] = it.value 150 event "StatusUpdate", ["No authentication for svn repo at ${it.key}"] 151 authManagerMap[repoType + "." + it.key] = aAuthManager 152 } 153 } 154 } 155 156 return repoMapTmp 157} 158 159//configure authentication for discovery repository 160pluginDiscoveryRepositories = configureAuth(pluginDiscoveryRepositories, "discovery") 161 162//configure authentication for distribution repository 163pluginDistributionRepositories = configureAuth(pluginDistributionRepositories, "distribution") 164 165/** 166 * Provide an authentication manager object. 167 * This method tries to use the configuration in settings.groovy 168 * (see comment of configureAuth for configuration pattern). If 169 * there's no configuration the user is prompted. 170 * @param repoKey 171 * @param repoType discovery or distribution 172 * @param tokenizeUrl the broken down url 173 * @return an ISVNAuthenticationManager impl instance building 174 */ 175private ISVNAuthenticationManager getAuthenticationManager(String repoKey, String repoType, Map tokenizeUrl) { 176 ISVNAuthenticationManager aAuthManager 177 usr = "user.svn.username.${repoType}.${repoKey}".toString() 178 psw = "user.svn.password.${repoType}.${repoKey}".toString() 179 if (tokenizeUrl[KEY_USER_NAME]) { 180 ant.antProject.setNewProperty usr, ""+tokenizeUrl[KEY_USER_NAME] 181 String pswd = tokenizeUrl[KEY_USER_PASS] ?: "" 182 ant.antProject.setNewProperty(psw, pswd) 183 } 184 //If no provided info, the user have to be prompt 185 ant.input(message:"Please enter your SVN username:", addproperty:usr) 186 ant.input(message:"Please enter your SVN password:", addproperty:psw) 187 def username = ant.antProject.getProperty(usr) 188 def password = ant.antProject.getProperty(psw) 189 authManager = SVNWCUtil.createDefaultAuthenticationManager(username , password) 190 //Test connection 191 aUrl = tokenizeUrl[KEY_URL] 192 def svnUrl = SVNURL.parseURIEncoded(aUrl) 193 def repo = SVNRepositoryFactory.create(svnUrl, null) 194 repo.authenticationManager = authManager 195 try { 196 repo.testConnection() 197 //only if it works... 198 repo.closeSession() 199 } 200 catch (SVNAuthenticationException ex) { 201 //GRAVE BAD CONFIGURATION : EXITING 202 event("StatusError",["Bad authentication configuration for $aUrl"]) 203 exit(1) 204 } 205 return authManager 206} 207 208/** 209 * Get an authenticationManager object starting from url 210 * @param url of SVN repo 211 * @param repoType discovery or distribution 212 * @return ISVNAuthenticationManager for SVN connection on on repo with auth 213 **/ 214private ISVNAuthenticationManager getAuthFromUrl(url, repoType) { 215 keyValue = repoType + "." + pluginDiscoveryRepositories.find { url.startsWith(it.value) }?.key 216 return authManagerMap[keyValue] 217} 218 219configureRepository = { targetRepoURL, String alias = "default" -> 220 repositoryName = alias 221 pluginsList = null 222 pluginsListFile = new File(grailsSettings.grailsWorkDir, "plugins-list-${alias}.xml") 223 224 def namedPluginSVN = pluginDistributionRepositories.find { it.key == alias }?.value 225 if (namedPluginSVN) { 226 pluginSVN = namedPluginSVN 227 } 228 else { 229 pluginSVN = DEFAULT_PUBLISH_URL 230 } 231 pluginDistURL = targetRepoURL 232 pluginBinaryDistURL = "$targetRepoURL/dist" 233 remotePluginList = "$targetRepoURL/.plugin-meta/plugins-list.xml" 234} 235 236configureRepository(DEFAULT_PLUGIN_DIST) 237 238configureRepositoryForName = { String targetRepository, type="discovery" -> 239 // Works around a bug in Groovy 1.5.6's DOMCategory that means get on Object returns null. Change to "pluginDiscoveryRepositories.targetRepository" when upgrading 240 def targetRepoURL = pluginDiscoveryRepositories.find { it.key == targetRepository }?.value 241 242 if (targetRepoURL) { 243 configureRepository(targetRepoURL, targetRepository) 244 } 245 else { 246 println "No repository configured for name ${targetRepository}. Set the 'grails.plugin.repos.${type}.${targetRepository}' variable to the location of the repository." 247 exit(1) 248 } 249} 250 251target ('default': "A target for plug-in developers that uploads and commits the current plug-in as the latest revision. The command will prompt for your SVN login details.") { 252 releasePlugin() 253} 254 255target(processAuth:"Prompts user for login details to create authentication manager") { 256 if (!authManager) { 257 def authKey = argsMap.repository ? "distribution.${argsMap.repository}".toString() : "distribution.default" 258 if (authManagerMap[authKey]) { 259 authManager = authManagerMap[authKey] 260 } 261 else { 262 usr = "user.svn.username.distribution" 263 psw = "user.svn.password.distribution" 264 if (argsMap.repository) { 265 usr = usr + "." + argsMap.repository 266 psw = psw + "." + argsMap.repository 267 } 268 else { 269 usr = usr + ".default" 270 psw = psw + ".default" 271 } 272 273 def (username, password) = [argsMap.username, argsMap.password] 274 275 if (!username) { 276 ant.input(message:"Please enter your SVN username:", addproperty:usr) 277 username = ant.antProject.getProperty(usr) 278 } 279 280 if (!password) { 281 ant.input(message:"Please enter your SVN password:", addproperty:psw) 282 password = ant.antProject.getProperty(psw) 283 } 284 285 authManager = SVNWCUtil.createDefaultAuthenticationManager(username , password) 286 authManagerMap.put(authKey,authManager) 287 } 288 } 289} 290 291target(checkLicense:"Checks the license file for the plugin exists") { 292 if (!(new File("${basedir}/LICENSE").exists()) && !(new File("${basedir}/LICENSE.txt").exists())) { 293 println "No LICENSE.txt file for plugin found. Please provide a license file containing the appropriate software licensing information (eg. Apache 2.0, GPL etc.)" 294 exit(1) 295 } 296} 297 298target(releasePlugin: "The implementation target") { 299 depends(parseArguments,checkLicense) 300 301 if (argsMap.skipMetadata != true) { 302 println "Generating plugin project behavior metadata..." 303 try { 304 MetadataGeneratingMetaClassCreationHandle.enable() 305 packageApp() 306 loadApp() 307 configureApp() 308 MetadataGeneratingMetaClassCreationHandle.disable() 309 } 310 catch (e) { 311 println "There was an error generating project behavior metadata: [${e.message}]" 312 } 313 } 314 packagePlugin() 315 if (argsMap.skipDocs != true) { 316 docs() 317 } 318 319 if (argsMap.packageOnly) { 320 return 321 } 322 processAuth() 323 324 if (argsMap.message) { 325 commitMessage = argsMap.message 326 } 327 328 if (argsMap.repository) { 329 configureRepositoryForName(argsMap.repository, "distribution") 330 } 331 if (argsMap.snapshot || argsMap.'skip-latest') { 332 skipLatest = true 333 } 334 remoteLocation = "${pluginSVN}/grails-${pluginName}" 335 trunk = SVNURL.parseURIDecoded("${remoteLocation}/trunk") 336 latestRelease = "${remoteLocation}/tags/LATEST_RELEASE" 337 versionedRelease = "${remoteLocation}/tags/RELEASE_${plugin.version.toString().replaceAll('\\.','_')}" 338 339 FSRepositoryFactory.setup() 340 DAVRepositoryFactory.setup() 341 SVNRepositoryFactoryImpl.setup() 342 343 try { 344 if (argsMap.pluginlist) { 345 commitNewGlobalPluginList() 346 } 347 else if (argsMap.zipOnly) { 348 publishZipOnlyRelease() 349 } 350 else { 351 def statusClient = new SVNStatusClient((ISVNAuthenticationManager)authManager,null) 352 353 try { 354 // get status of base directory, if this fails exception will be thrown 355 statusClient.doStatus(baseFile, true) 356 updateAndCommitLatest() 357 } 358 catch (SVNException ex) { 359 // error with status, not in repo, attempt import. 360 if (ex.message.contains("is not a working copy")) { 361 boolean notInRepository = isPluginNotInRepository() 362 if (notInRepository) { 363 importToSVN() 364 } 365 else { 366 def result = confirmInput(""" 367The current directory is not a working copy and your latest changes won't be committed. 368You need to checkout a working copy and make your changes there. 369Alternatively, would you like to publish a zip-only (no sources) release?""") 370 if (!result) exit(0) 371 372 publishZipOnlyRelease() 373 return 374 } 375 } 376 else { 377 event('StatusFinal', ["Failed to stat working directory: ${ex.message}"]) 378 exit(1) 379 } 380 } 381 tagPluginRelease() 382 modifyOrCreatePluginList() 383 event('StatusFinal', ["Plug-in release successfully published"]) 384 } 385 } 386 catch(Exception e) { 387 logErrorAndExit("Error occurred with release-plugin", e) 388 } 389} 390 391def publishZipOnlyRelease() { 392 def localWorkingCopy = new File("${projectWorkDir}/working-copy") 393 ant.mkdir(dir: localWorkingCopy) 394 395 if (isPluginNotInRepository()) { 396 updateLocalZipAndXml(localWorkingCopy) 397 importBaseToSVN(localWorkingCopy) 398 } 399 cleanLocalWorkingCopy(localWorkingCopy) 400 checkoutFromSVN(localWorkingCopy, trunk) 401 updateLocalZipAndXml(localWorkingCopy) 402 addPluginZipAndMetadataIfNeccessary(new File("${localWorkingCopy}/plugin.xml"), new File("${localWorkingCopy}/${new File(pluginZip).name}")) 403 commitDirectoryToSVN(localWorkingCopy) 404 405 tagPluginRelease() 406 modifyOrCreatePluginList() 407 deleteZipFromTrunk() 408 println "Successfully published zip-only plugin release." 409} 410 411def deleteZipFromTrunk() { 412 def commitClient = new SVNCommitClient((ISVNAuthenticationManager) authManager, null) 413 414 if (!commitMessage) askForMessage() 415 if(pluginZip) { 416 def pluginZipFile = new File(pluginZip) 417 def zipLocation = SVNURL.parseURIDecoded("${remoteLocation}/trunk/${pluginZipFile.name}") 418 try { commitClient.doDelete([zipLocation] as SVNURL[], commitMessage) } 419 catch (SVNException e) { 420 // ok - the zip doesn't exist yet 421 } 422 } 423} 424 425def updateLocalZipAndXml(File localWorkingCopy) { 426 ant.copy(file: pluginZip, todir: localWorkingCopy, overwrite:true) 427 ant.copy(file: "${basedir}/plugin.xml", todir: localWorkingCopy, overwrite:true) 428} 429 430def cleanLocalWorkingCopy(File localWorkingCopy) { 431 ant.delete(dir: localWorkingCopy) 432 ant.mkdir(dir: localWorkingCopy) 433} 434 435boolean isPluginNotInRepository() { 436 // Now check whether the plugin is in the repository. 437 // If not, we ask the user whether they want to import it. 438 SVNRepository repos = SVNRepositoryFactory.create(SVNURL.parseURIDecoded(pluginSVN)) 439 if(authManager != null) 440 repos.authenticationManager = authManager 441 boolean notInRepository = true 442 try { 443 notInRepository = !repos.info("grails-$pluginName", -1) 444 } 445 catch (e) { 446 // ignore 447 } 448 return notInRepository 449} 450 451target(modifyOrCreatePluginList:"Updates the remote plugin.xml descriptor or creates a new one in the repo") { 452 withPluginListUpdate { 453 ant.delete(file:pluginsListFile) 454 // get newest version of plugin list 455 try { 456 fetchRemoteFile("${pluginSVN}/.plugin-meta/plugins-list.xml", pluginsListFile) 457 } 458 catch (Exception e) { 459 println "Error reading remote plugin list [${e.message}], building locally..." 460 updatePluginsListManually() 461 } 462 463 def remoteRevision = "0" 464 if (shouldUseSVNProtocol(pluginDistURL)) { 465 withSVNRepo(pluginDistURL) { repo -> 466 remoteRevision = repo.getLatestRevision().toString() 467 } 468 } 469 else { 470 new URL(pluginDistURL).withReader { Reader reader -> 471 def line = reader.readLine() 472 line.eachMatch(/Revision (.*):/) { 473 remoteRevision = it[1] 474 } 475 } 476 } 477 478 def publisher = new DefaultPluginPublisher(remoteRevision, pluginDistURL) 479 def updatedList = publisher.publishRelease(pluginName, new FileSystemResource(pluginsListFile), !skipLatest) 480 pluginsListFile.withWriter("UTF-8") { w -> 481 publisher.writePluginList(updatedList, w) 482 } 483 } 484} 485 486target(commitNewGlobalPluginList:"updates the plugins.xml descriptor stored in the repo") { 487 withPluginListUpdate { 488 ant.delete(file:pluginsListFile) 489 println "Building plugin list for commit..." 490 updatePluginsListManually() 491 } 492} 493 494private withPluginListUpdate(Closure updateLogic) { 495 if (!commitMessage) { 496 askForMessage() 497 } 498 updateLogic() 499 500 def pluginMetaDir = new File("${grailsSettings.grailsWorkDir}/${repositoryName}/.plugin-meta") 501 def updateClient = new SVNUpdateClient((ISVNAuthenticationManager)authManager, null) 502 def importClient = new SVNCommitClient((ISVNAuthenticationManager)authManager, null) 503 def addClient = new SVNWCClient((ISVNAuthenticationManager)authManager, null) 504 505 String remotePluginMetadata = "${pluginSVN}/.plugin-meta" 506 if (!pluginMetaDir.exists()) { 507 println "Checking out locally to '${pluginMetaDir}'." 508 checkoutOrImportPluginMetadata(pluginMetaDir, remotePluginMetadata, updateClient, importClient) 509 } 510 else { 511 try { 512 updateClient.doUpdate(pluginMetaDir, SVNRevision.HEAD, true) 513 commitNewestPluginList(pluginMetaDir, importClient) 514 } 515 catch (SVNException e) { 516 println "Plugin meta directory corrupt, checking out again" 517 checkoutOrImportPluginMetadata(pluginMetaDir, remotePluginMetadata, updateClient, importClient) 518 } 519 } 520} 521 522private checkoutOrImportPluginMetadata (File pluginMetaDir, String remotePluginMetadata, SVNUpdateClient updateClient, SVNCommitClient importClient) { 523 def svnURL = SVNURL.parseURIDecoded (remotePluginMetadata) 524 try { 525 updateClient.doCheckout(svnURL, pluginMetaDir, SVNRevision.HEAD, SVNRevision.HEAD, true) 526 527 try { 528 addClient.doAdd(new File("$pluginMetaDir/plugins-list.xml"), false, false, false, false) 529 } 530 catch (e) { 531 // ignore 532 } 533 commitNewestPluginList(pluginMetaDir, importClient) 534 } 535 catch (SVNException e) { 536 println "Importing plugin meta data to ${remotePluginMetadata}. Please wait..." 537 538 ant.mkdir(dir: pluginMetaDir) 539 ant.copy(file: pluginsListFile, tofile: "$pluginMetaDir/plugins-list.xml") 540 541 def commit = importClient.doImport(pluginMetaDir, svnURL, commitMessage, true) 542 println "Committed revision ${commit.newRevision} of plugins-list.xml." 543 ant.delete(dir: pluginMetaDir) 544 updateClient.doCheckout(svnURL, pluginMetaDir, SVNRevision.HEAD, SVNRevision.HEAD, true) 545 } 546} 547 548private def commitNewestPluginList(File pluginMetaDir, SVNCommitClient importClient) { 549 ant.copy(file: pluginsListFile, tofile: "$pluginMetaDir/plugins-list.xml", overwrite: true) 550 551 def commit = importClient.doCommit([pluginMetaDir] as File[], false, commitMessage, true, true) 552 553 println "Committed revision ${commit.newRevision} of plugins-list.xml." 554} 555 556target(checkInPluginZip:"Checks in the plug-in zip if it has not been checked in already") { 557 558 def pluginXml = new File("${basedir}/plugin.xml") 559 560 addPluginZipAndMetadataIfNeccessary(pluginXml, new File(pluginZip)) 561} 562 563def addPluginZipAndMetadataIfNeccessary(File pluginXml, File pluginFile) { 564 def statusClient = new SVNStatusClient((ISVNAuthenticationManager) authManager, null) 565 def wcClient = new SVNWCClient((ISVNAuthenticationManager) authManager, null) 566 567 def addPluginFile = false 568 try { 569 def status = statusClient.doStatus(pluginFile, true) 570 if (status.kind == SVNNodeKind.NONE || status.kind == SVNNodeKind.UNKNOWN) addPluginFile = true 571 } 572 catch (SVNException) { 573 // not checked in add and commit 574 addPluginFile = true 575 } 576 if (addPluginFile) wcClient.doAdd(pluginFile, true, false, false, false) 577 addPluginFile = false 578 try { 579 def status = statusClient.doStatus(pluginXml, true) 580 if (status.kind == SVNNodeKind.NONE || status.kind == SVNNodeKind.UNKNOWN) addPluginFile = true 581 } 582 catch (SVNException e) { 583 addPluginFile = true 584 } 585 if (addPluginFile) wcClient.doAdd(pluginXml, true, false, false, false) 586} 587 588target(updateAndCommitLatest:"Commits the latest revision of the Plug-in") { 589 def result = !isInteractive || confirmInput(""" 590This command will perform the following steps to release your plug-in to Grails' SVN repository: 591* Update your sources to the HEAD revision 592* Commit any changes you've made to SVN 593* Tag the release 594 595NOTE: This command will not add new resources for you, if you have additional sources to add please run 'svn add' before running this command. 596NOTE: Make sure you have updated the version number in your *GrailsPlugin.groovy descriptor. 597 598Are you sure you wish to proceed? 599""") 600 if (!result) exit(0) 601 602 println "Checking in plugin zip..." 603 checkInPluginZip() 604 605 long r = updateDirectoryFromSVN(baseFile) 606 println "Updated to revision ${r}. Committing local, please wait..." 607 608 def commit = commitDirectoryToSVN(baseFile) 609 610 println "Committed revision ${commit.newRevision}." 611} 612 613def commitDirectoryToSVN(baseFile) { 614 commitClient = new SVNCommitClient((ISVNAuthenticationManager) authManager, null) 615 616 if (!commitMessage) askForMessage() 617 618 println "Committing code. Please wait..." 619 620 return commitClient.doCommit([baseFile] as File[], false, commitMessage, true, true) 621} 622 623long updateDirectoryFromSVN(baseFile) { 624 updateClient = new SVNUpdateClient((ISVNAuthenticationManager) authManager, null) 625 626 println "Updating from SVN..." 627 long r = updateClient.doUpdate(baseFile, SVNRevision.HEAD, true) 628 return r 629} 630 631target(importToSVN:"Imports a plugin project to Grails' remote SVN repository") { 632 File checkOutDir = new File("${baseFile.parentFile.absolutePath}/checkout/${baseFile.name}") 633 634 ant.unzip(src:pluginZip, dest:"${basedir}/unzipped") 635 ant.copy(file:pluginZip, todir:"${basedir}/unzipped") 636 ant.mkdir(dir:"${basedir}/unzipped/grails-app") 637 638 File importBaseDirectory = new File("${basedir}/unzipped") 639 640 String testsDir = "${importBaseDirectory}/test" 641 ant.mkdir(dir:testsDir) 642 ant.copy(todir:testsDir) { 643 fileset(dir:"${grailsSettings.testSourceDir}") 644 } 645 646 try { 647 def result = !isInteractive || confirmInput(""" 648 This plug-in project is not currently in the repository, this command will now: 649 * Perform an SVN import into the repository 650 * Checkout the imported version of the project from SVN to '${checkOutDir}' 651 * Tag the plug-in project as the LATEST_RELEASE 652 Are you sure you wish to proceed? 653 """) 654 if (!result) { 655 ant.delete(dir:importBaseDirectory, failonerror:false) 656 exit(0) 657 } 658 svnURL = importBaseToSVN(importBaseDirectory) 659 } 660 finally { 661 ant.delete(dir:importBaseDirectory, failonerror:false) 662 } 663 checkOutDir.parentFile.mkdirs() 664 665 checkoutFromSVN(checkOutDir, svnURL) 666 667 event('StatusFinal', [""" 668Completed SVN project import. If you are in terminal navigate to imported project with: 669cd ${checkOutDir} 670 671Future changes should be made to the SVN controlled sources!"""]) 672} 673 674def checkoutFromSVN(File checkOutDir, SVNURL svnURL) { 675 updateClient = new SVNUpdateClient((ISVNAuthenticationManager) authManager, null) 676 println "Checking out locally to '${checkOutDir}'." 677 updateClient.doCheckout(svnURL, checkOutDir, SVNRevision.HEAD, SVNRevision.HEAD, true) 678} 679 680SVNURL importBaseToSVN(File importBaseDirectory) { 681 importClient = new SVNCommitClient((ISVNAuthenticationManager) authManager, null) 682 if (!commitMessage) askForMessage() 683 684 println "Importing project to ${remoteLocation}. Please wait..." 685 686 def svnURL = SVNURL.parseURIDecoded("${remoteLocation}/trunk") 687 importClient.doImport(importBaseDirectory, svnURL, commitMessage, true) 688 println "Plug-in project imported to SVN at location '${remoteLocation}/trunk'" 689 return svnURL 690} 691 692target(tagPluginRelease:"Tags a plugin-in with the LATEST_RELEASE tag and version tag within the /tags area of SVN") { 693 println "Preparing to publish the release..." 694 695 def copyClient = new SVNCopyClient((ISVNAuthenticationManager) authManager, null) 696 def commitClient = new SVNCommitClient((ISVNAuthenticationManager) authManager, null) 697 698 if (!commitMessage) askForMessage() 699 700 tags = SVNURL.parseURIDecoded("${remoteLocation}/tags") 701 latest = SVNURL.parseURIDecoded(latestRelease) 702 release = SVNURL.parseURIDecoded(versionedRelease) 703 704 try { commitClient.doMkDir([tags] as SVNURL[], commitMessage) } 705 catch (SVNException e) { 706 // ok - already exists 707 } 708 if (!skipLatest) { 709 try { commitClient.doDelete([latest] as SVNURL[], commitMessage) } 710 catch (SVNException e) { 711 // ok - the tag doesn't exist yet 712 } 713 } 714 try { commitClient.doDelete([release] as SVNURL[], commitMessage) } 715 catch (SVNException e) { 716 // ok - the tag doesn't exist yet 717 } 718 719 // Get remote URL for this working copy. 720 def wcClient = new SVNWCClient((ISVNAuthenticationManager) authManager, null) 721 def copyFromUrl = trunk 722 723 // First tag this release with the version number. 724 try { 725 println "Tagging version release, please wait..." 726 def copySource = new SVNCopySource(SVNRevision.HEAD, SVNRevision.HEAD, copyFromUrl) 727 def commit = copyClient.doCopy([copySource] as SVNCopySource[], release, false, false, true, commitMessage, new SVNProperties()) 728 println "Copied trunk to ${versionedRelease} with revision ${commit.newRevision} on ${commit.date}" 729 730 // And now make it the latest release. 731 if (!skipLatest) { 732 println "Tagging latest release, please wait..." 733 copySource = new SVNCopySource(SVNRevision.HEAD, SVNRevision.HEAD, release) 734 commit = copyClient.doCopy([copySource] as SVNCopySource[], latest, false, false, true, commitMessage, new SVNProperties()) 735 println "Copied trunk to ${latestRelease} with revision ${commit.newRevision} on ${commit.date}" 736 } 737 } 738 catch (SVNException e) { 739 logErrorAndExit("Error tagging release", e) 740 } 741} 742 743target(askForMessage:"Asks for the users commit message") { 744 ant.input(message:"Enter a SVN commit message:", addproperty:"commit.message") 745 commitMessage = ant.antProject.properties."commit.message" 746} 747 748target(updatePluginsListManually: "Updates the plugin list by manually reading each URL, the slow way") { 749 depends(configureProxy) 750 try { 751 def recreateCache = false 752 document = null 753 if (!pluginsListFile.exists()) { 754 println "Plugins list cache doesn't exist creating.." 755 recreateCache = true 756 } 757 else { 758 try { 759 document = DOMBuilder.parse(new FileReader(pluginsListFile)) 760 } 761 catch (Exception e) { 762 recreateCache = true 763 println "Plugins list cache is corrupt [${e.message}]. Re-creating.." 764 } 765 } 766 if (recreateCache) { 767 document = DOMBuilder.newInstance().createDocument() 768 def root = document.createElement('plugins') 769 root.setAttribute('revision', '0') 770 document.appendChild(root) 771 } 772 773 pluginsList = document.documentElement 774 builder = new DOMBuilder(document) 775 776 def localRevision = pluginsList ? new Integer(pluginsList.getAttribute('revision')) : -1 777 // extract plugins svn repository revision - used for determining cache up-to-date 778 def remoteRevision = 0 779 try { 780 // determine if this is a secure plugin spot.. 781 if (shouldUseSVNProtocol(pluginDistURL)) { 782 withSVNRepo(pluginDistURL) { repo -> 783 remoteRevision = repo.getLatestRevision() 784 if (remoteRevision > localRevision) { 785 // Plugins list cache is expired, need to update 786 event("StatusUpdate", ["Plugins list cache has expired. Updating, please wait"]) 787 pluginsList.setAttribute('revision', remoteRevision as String) 788 repo.getDir('', -1,null,(Collection)null).each() { entry -> 789 final String PREFIX = "grails-" 790 if (entry.name.startsWith(PREFIX)) { 791 def pluginName = entry.name.substring(PREFIX.length()) 792 buildPluginInfo(pluginsList, pluginName) 793 } 794 } 795 } 796 } 797 } 798 else { 799 new URL(pluginDistURL).withReader { Reader reader -> 800 def line = reader.readLine() 801 line.eachMatch(/Revision (.*):/) { 802 remoteRevision = it[1].toInteger() 803 } 804 if (remoteRevision > localRevision) { 805 // Plugins list cache is expired, need to update 806 event("StatusUpdate", ["Plugins list cache has expired. Updating, please wait"]) 807 pluginsList.setAttribute('revision', remoteRevision as String) 808 // for each plugin directory under Grails Plugins SVN in form of 'grails-*' 809 while (line = reader.readLine()) { 810 line.eachMatch(/<li><a href="grails-(.+?)">/) { 811 // extract plugin name 812 def pluginName = it[1][0..-2] 813 // collect information about plugin 814 buildPluginInfo(pluginsList, pluginName) 815 } 816 } 817 } 818 } 819 } 820 } 821 catch (Exception e) { 822 event("StatusError", ["Unable to list plugins, please check you have a valid internet connection: ${e.message}" ]) 823 } 824 825 // update plugins list cache file 826 writePluginsFile() 827 } 828 catch (Exception e) { 829 event("StatusError", ["Unable to list plugins, please check you have a valid internet connection: ${e.message}" ]) 830 } 831} 832 833def buildPluginInfo(root, pluginName) { 834 use(DOMCategory) { 835 // determine the plugin node.. 836 def pluginNode = root.'plugin'.find {it.'@name' == pluginName} 837 if (!pluginNode) { 838 // add it if it doesn't exist.. 839 pluginNode = builder.'plugin'(name: pluginName) 840 root.appendChild(pluginNode) 841 } 842 843 // add each of the releases.. 844 event("StatusUpdate", ["Reading [$pluginName] plugin info"]) 845 // determine if this is a secure plugin spot.. 846 def tagsUrl = "${pluginDistURL}/grails-${pluginName}/tags" 847 try { 848 if (shouldUseSVNProtocol(pluginDistURL)) { 849 withSVNRepo(tagsUrl) { repo -> 850 repo.getDir('',-1,null,(Collection)null).each() { entry -> 851 buildReleaseInfo(pluginNode, pluginName, tagsUrl, entry.name) 852 } 853 } 854 } 855 else { 856 def releaseTagsList = new URL(tagsUrl).text 857 releaseTagsList.eachMatch(/<li><a href="(.+?)">/) { 858 def releaseTag = it[1][0..-2] 859 buildReleaseInfo(pluginNode, pluginName, tagsUrl, releaseTag) 860 } 861 } 862 } 863 catch(e) { 864 // plugin has not tags 865 println "Plugin [$pluginName] doesn't have any tags" 866 } 867 868 try { 869 def latestRelease = null 870 def url = "${pluginDistURL}/grails-${pluginName}/tags/LATEST_RELEASE/plugin.xml" 871 fetchPluginListFile(url).withReader {Reader reader -> 872 def line = reader.readLine() 873 line.eachMatch (/.+?version='(.+?)'.+/) { 874 latestRelease = it[1] 875 } 876 } 877 if (latestRelease && pluginNode.'release'.find {it.'@version' == latestRelease}) { 878 pluginNode.setAttribute('latest-release', latestRelease as String) 879 } 880 } 881 catch(e) { 882 // plugin doesn't have a latest release 883 println "Plugin [$pluginName] doesn't have a latest release" 884 } 885 } 886} 887 888def buildReleaseInfo(root, pluginName, releasePath, releaseTag) { 889 // quick exit nothing to do.. 890 if (releaseTag == '..' || releaseTag == 'LATEST_RELEASE') { 891 return 892 } 893 894 // remove previous version.. 895 def releaseNode = root.'release'.find() { 896 it.'@tag' == releaseTag && it.'&type' == 'svn' 897 } 898 if (releaseNode) { 899 root.removeChild(releaseNode) 900 } 901 try { 902 903 // copy the properties to the new node.. 904 def releaseUrl = "${releasePath}/${releaseTag}" 905 def properties = ['title', 'author', 'authorEmail', 'description', 'documentation'] 906 def releaseDescriptor = parseRemoteXML("${releaseUrl}/plugin.xml").documentElement 907 def version = releaseDescriptor.'@version' 908 909 releaseNode = builder.createNode('release', [tag: releaseTag, version: version, type: 'svn']) 910 root.appendChild(releaseNode) 911 properties.each { 912 if (releaseDescriptor."${it}") { 913 releaseNode.appendChild(builder.createNode(it, releaseDescriptor."${it}".text())) 914 } 915 } 916 releaseNode.appendChild(builder.createNode('file', "${releaseUrl}/grails-${pluginName}-${version}.zip")) 917 } 918 catch(e) { 919 // no release info available, probably an older plugin with no plugin.xml defined. 920 } 921} 922 923def parsePluginList() { 924 if (pluginsList == null) { 925 profile("Reading local plugin list from $pluginsListFile") { 926 def document 927 try { 928 document = DOMBuilder.parse(new FileReader(pluginsListFile)) 929 } 930 catch (Exception e) { 931 println "Plugin list file corrupt, retrieving again.." 932 readRemotePluginList() 933 document = DOMBuilder.parse(new FileReader(pluginsListFile)) 934 } 935 pluginsList = document.documentElement 936 } 937 } 938} 939 940def readRemotePluginList() { 941 ant.delete(file:pluginsListFile, failonerror:false) 942 ant.mkdir(dir:pluginsListFile.parentFile) 943 fetchRemoteFile(remotePluginList, pluginsListFile) 944} 945 946def writePluginsFile() { 947 pluginsListFile.parentFile.mkdirs() 948 949 Transformer transformer = TransformerFactory.newInstance().newTransformer() 950 transformer.setOutputProperty(OutputKeys.INDENT, "true") 951 transformer.transform(new DOMSource(document), new StreamResult(pluginsListFile)) 952} 953 954shouldUseSVNProtocol = { pluginDistURL -> 955 return isSecureUrl(pluginDistURL) || pluginDistURL.startsWith("file://") 956} 957 958def parseRemoteXML(url) { 959 fetchPluginListFile(url).withReader() { DOMBuilder.parse(it) } 960} 961 962/** 963 * Downloads a remote plugin zip into the plugins dir 964 */ 965downloadRemotePlugin = { url, pluginsBase -> 966 def slash = url.file.lastIndexOf('/') 967 def fullPluginName = "${url.file[slash + 8..-5]}" 968 String zipLocation = "${pluginsBase}/grails-${fullPluginName}.zip" 969 fetchRemoteFile("${url}", zipLocation) 970 readMetadataFromZip(zipLocation, url) 971 return fullPluginName 972} 973 974fetchRemoteFile = { url, destfn -> 975 if (shouldUseSVNProtocol(pluginDistURL)) { 976 // fetch the remote file.. 977 fetchRemote(url) { repo, file -> 978 // get the latest file from the repository.. 979 def f = (destfn instanceof File) ? destfn : new File(destfn) 980 f.withOutputStream() { os -> 981 def props = new SVNProperties() 982 repo.getFile(file , (long)-1L, props , os) 983 } 984 } 985 } 986 else { 987 ant.get(src:url, dest:destfn, verbose:"yes", usetimestamp:true) 988 } 989} 990 991/** 992 * Fetch the entire plugin list file. 993 */ 994fetchPluginListFile = { url -> 995 // attempt to fetch the file using SVN. 996 if (shouldUseSVNProtocol(pluginDistURL)) { 997 def rdr = fetchRemote(url) { repo, file -> 998 // get the latest file from the repository.. 999 def props = new SVNProperties() 1000 def baos = new ByteArrayOutputStream() 1001 def ver = repo.getFile(file , (long)-1L, props , baos) 1002 def mimeType = props.getSVNPropertyValue(SVNProperty.MIME_TYPE) 1003 if (!SVNProperty.isTextMimeType(mimeType)) { 1004 throw new Exception("Must be a text file..") 1005 } 1006 return new StringReader(new String(baos.toByteArray(), 'utf-8')) 1007 } 1008 return rdr 1009 } 1010 // attempt using URL 1011 return new URL(url) 1012} 1013 1014def fetchRemote(url, closure) { 1015 def idx = url.lastIndexOf('/') 1016 def svnUrl = url.substring(0,idx) 1017 def file = url.substring(idx+1,url.length()) 1018 1019 withSVNRepo(svnUrl) { repo -> 1020 // determine if the file exists 1021 SVNNodeKind nodeKind = repo.checkPath(file , -1) 1022 if (nodeKind == SVNNodeKind.NONE) { 1023 throw new Exception("The file does not exist.: " + url) 1024 } 1025 if (nodeKind != SVNNodeKind.FILE) { 1026 throw new Exception("Error not a file..: " + url) 1027 } 1028 // present w/ file etc for repo extraction 1029 closure.call(repo, file) 1030 } 1031} 1032 1033def isSecureUrl(Object url) { 1034 url.startsWith('https://') || url.startsWith('svn://') 1035} 1036 1037withSVNRepo = { url, closure -> 1038 // create a authetication manager using the defaults 1039 ISVNAuthenticationManager authMgr = getAuthFromUrl(url,"discovery") 1040 1041 // create the url 1042 def svnUrl = SVNURL.parseURIEncoded(url) 1043 def repo = SVNRepositoryFactory.create(svnUrl, null) 1044 repo.authenticationManager = authMgr 1045 // trigger authentication failure?.?. 1046 try { 1047 repo.getLatestRevision() 1048 } 1049 catch (SVNAuthenticationException ex) { 1050 event "StatusUpdate", ["Default authentication failed please enter credentials."] 1051 // prompt for login information.. 1052 ant.input(message:"Please enter your SVN username:", addproperty:"user.svn.username") 1053 ant.input(message:"Please enter your SVN password:", addproperty:"user.svn.password") 1054 def username = ant.antProject.properties."user.svn.username" 1055 def password = ant.antProject.properties."user.svn.password" 1056 authMgr = SVNWCUtil.createDefaultAuthenticationManager(username , password) 1057 repo.setAuthenticationManager(authMgr) 1058 // don't bother to catch this one let it bubble up.. 1059 repo.getLatestRevision() 1060 } 1061 // make sure the closure return is returned.. 1062 closure.call(repo) 1063} 1064