From 2b2ee34b68f1894d74a36c4742ab9993c54498f3 Mon Sep 17 00:00:00 2001 From: Graeme Rocher Date: Mon, 17 Aug 2009 18:40:02 +0200 Subject: [PATCH] new plugin publishing mechanism that eliminates the huge amount of time it takes to publish a plugin and also fixes the problem where the plugin list could get corrupted --- grails/scripts/ReleasePlugin.groovy | 56 +++- grails/scripts/_GrailsCreateProject.groovy | 1 + grails/scripts/_PluginDependencies.groovy | 8 +- .../templates/plugins/GrailsPlugin.groovy | 2 +- .../publishing/DefaultPluginPublisher.groovy | 140 +++++++++ .../DefaultPluginPublisherTests.groovy | 273 ++++++++++++++++++ 6 files changed, 468 insertions(+), 12 deletions(-) create mode 100644 grails/src/java/org/codehaus/groovy/grails/plugins/publishing/DefaultPluginPublisher.groovy create mode 100644 grails/src/test/org/codehaus/groovy/grails/plugins/publishing/DefaultPluginPublisherTests.groovy diff --git a/grails/scripts/ReleasePlugin.groovy b/grails/scripts/ReleasePlugin.groovy index 78e048391..d7ca6b345 100644 --- a/grails/scripts/ReleasePlugin.groovy +++ b/grails/scripts/ReleasePlugin.groovy @@ -17,11 +17,15 @@ import org.tmatesoft.svn.core.io.SVNRepositoryFactory import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory +import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl import org.tmatesoft.svn.core.io.* import org.tmatesoft.svn.core.* import org.tmatesoft.svn.core.auth.* import org.tmatesoft.svn.core.wc.* import org.codehaus.groovy.grails.documentation.MetadataGeneratingMetaClassCreationHandle +import org.codehaus.groovy.grails.plugins.GrailsPluginUtils +import org.codehaus.groovy.grails.plugins.publishing.DefaultPluginPublisher +import org.springframework.core.io.FileSystemResource /** * Gant script that handles releasing plugins to a plugin repository. @@ -66,7 +70,7 @@ target(processAuth:"Prompts user for login details to create authentication mana def username = ant.antProject.getProperty(usr) def password = ant.antProject.getProperty(psw) authManager = SVNWCUtil.createDefaultAuthenticationManager( username , password ) - authManagerMap.put(authKey,authManager) + authManagerMap.put(authKey,authManager) } } @@ -87,7 +91,10 @@ target(releasePlugin: "The implementation target") { } packagePlugin() - docs() + if(argsMap.skipDocs != true) { + docs() + } + if(argsMap.packageOnly) { return @@ -108,10 +115,11 @@ target(releasePlugin: "The implementation target") { FSRepositoryFactory.setup() DAVRepositoryFactory.setup() + SVNRepositoryFactoryImpl.setup() try { if(argsMap.pluginlist) { - commitNewGlobalPluginList() + modifyOrCreatePluginList() } else { def statusClient = new SVNStatusClient((ISVNAuthenticationManager)authManager,null) @@ -161,14 +169,48 @@ a working copy and make your changes there. Alternatively, do you want to procee } } +target(modifyOrCreatePluginList:"Updates the remote plugin.xml descriptor or creates a new one in the repo") { + withPluginListUpdate { + ant.delete(file:pluginsListFile) + // get newest version of plugin list + fetchRemoteFile("${pluginSVN}/.plugin-meta/plugins-list.xml", pluginsListFile) + + def remoteRevision = "0" + if (shouldUseSVNProtocol(pluginDistURL)) { + withSVNRepo(pluginDistURL) { repo -> + remoteRevision = repo.getLatestRevision().toString() + } + } + else { + new URL(pluginDistURL).withReader { Reader reader -> + def line = reader.readLine() + line.eachMatch(/Revision (.*):/) { + remoteRevision = it[1] + } + } + } + + def publisher = new DefaultPluginPublisher(remoteRevision) + def updatedList = publisher.publishRelease(pluginName, new FileSystemResource(pluginsListFile), !skipLatest) + pluginsListFile.withWriter { w -> + publisher.writePluginList(updatedList, w) + } + } +} + target(commitNewGlobalPluginList:"updates the plugins.xml descriptor stored in the repo") { + withPluginListUpdate { + ant.delete(file:pluginsListFile) + println "Building plugin list for commit..." + updatePluginsListManually() + } +} +private withPluginListUpdate(Closure updateLogic) { if(!commitMessage) { askForMessage() } - ant.delete(file:pluginsListFile) - println "Building plugin list for commit..." - updatePluginsListManually() + updateLogic() def pluginMetaDir = new File("${grailsSettings.grailsWorkDir}/${repositoryName}/.plugin-meta") def updateClient = new SVNUpdateClient((ISVNAuthenticationManager)authManager, null) @@ -189,8 +231,8 @@ target(commitNewGlobalPluginList:"updates the plugins.xml descriptor stored in t checkoutOrImportPluginMetadata(pluginMetaDir, remotePluginMetadata, updateClient, importClient) } } -} +} private checkoutOrImportPluginMetadata (File pluginMetaDir, String remotePluginMetadata, SVNUpdateClient updateClient, SVNCommitClient importClient) { def svnURL = SVNURL.parseURIDecoded (remotePluginMetadata) try { diff --git a/grails/scripts/_GrailsCreateProject.groovy b/grails/scripts/_GrailsCreateProject.groovy index 29dfe5c02..680b0f5ce 100644 --- a/grails/scripts/_GrailsCreateProject.groovy +++ b/grails/scripts/_GrailsCreateProject.groovy @@ -80,6 +80,7 @@ target(createPlugin: "The implementation target") { include(name: "*GrailsPlugin.groovy") include(name: "scripts/*") replacefilter(token: "@plugin.name@", value: pluginName) + replacefilter(token: "@plugin.short.name@", value: GrailsNameUtils.getScriptName(pluginName)) replacefilter(token: "@plugin.version@", value: grailsAppVersion ?: "0.1") replacefilter(token: "@grails.version@", value: grailsVersion) } diff --git a/grails/scripts/_PluginDependencies.groovy b/grails/scripts/_PluginDependencies.groovy index c2024dbf9..d2b269f60 100644 --- a/grails/scripts/_PluginDependencies.groovy +++ b/grails/scripts/_PluginDependencies.groovy @@ -681,7 +681,7 @@ target(updatePluginsListManually: "Updates the plugin list by manually reading e } } -boolean shouldUseSVNProtocol(pluginDistURL) { +shouldUseSVNProtocol = { pluginDistURL -> return isSecureUrl(pluginDistURL) || pluginDistURL.startsWith("file://") } @@ -1240,7 +1240,7 @@ def buildPluginInfo(root, pluginName) { } } -def fetchRemoteFile(url, destfn) { +fetchRemoteFile = { url, destfn -> if (shouldUseSVNProtocol(pluginDistURL)) { // fetch the remote file.. fetchRemote(url) { repo, file -> @@ -1259,7 +1259,7 @@ def fetchRemoteFile(url, destfn) { /** * Fetch the entire plugin list file. */ -def fetchPluginListFile(url) { +fetchPluginListFile = { url -> // attempt to fetch the file using SVN. if (shouldUseSVNProtocol(pluginDistURL)) { def rdr = fetchRemote(url) { repo, file -> @@ -1304,7 +1304,7 @@ def isSecureUrl(Object url) { url.startsWith('https://') || url.startsWith('svn://') } -def withSVNRepo(url, closure) { +withSVNRepo = { url, closure -> // create a authetication manager using the defaults ISVNAuthenticationManager authMgr = getAuthFromUrl(url,"discovery") diff --git a/grails/src/grails/templates/plugins/GrailsPlugin.groovy b/grails/src/grails/templates/plugins/GrailsPlugin.groovy index 41fbcb42a..67564d4ae 100644 --- a/grails/src/grails/templates/plugins/GrailsPlugin.groovy +++ b/grails/src/grails/templates/plugins/GrailsPlugin.groovy @@ -19,7 +19,7 @@ Brief description of the plugin. ''' // URL to the plugin's documentation - def documentation = "http://grails.org/@plugin.name@+Plugin" + def documentation = "http://grails.org/plugin/@plugin.short.name@" def doWithWebDescriptor = { xml -> // TODO Implement additions to web.xml (optional), this event occurs before diff --git a/grails/src/java/org/codehaus/groovy/grails/plugins/publishing/DefaultPluginPublisher.groovy b/grails/src/java/org/codehaus/groovy/grails/plugins/publishing/DefaultPluginPublisher.groovy new file mode 100644 index 000000000..a2211496a --- /dev/null +++ b/grails/src/java/org/codehaus/groovy/grails/plugins/publishing/DefaultPluginPublisher.groovy @@ -0,0 +1,140 @@ +/* + * Copyright 2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.codehaus.groovy.grails.plugins.publishing + +import org.codehaus.groovy.grails.plugins.GrailsPluginUtils +import groovy.util.slurpersupport.GPathResult +import groovy.xml.MarkupBuilder +import org.springframework.core.io.Resource +import grails.util.BuildSettingsHolder + +/** + * Utility methods for manipulating the plugin-list.xml file used + * when publishing plugins to a Grails plugin repository + * + * @author Graeme Rocher + * @since 1.2 + */ + +public class DefaultPluginPublisher { + + String revision = "0" + DefaultPluginPublisher(String revNumber) { + if(revNumber) + this.revision = revNumber + } + + void writePluginList(GPathResult pluginList, Writer targetWriter) { + def stringWriter = new StringWriter() + stringWriter << new groovy.xml.StreamingMarkupBuilder().bind { + mkp.yield pluginList + } + new XmlNodePrinter(new PrintWriter(targetWriter)).print(new XmlParser().parseText(stringWriter.toString())) + } + + /** + * Publishes a plugin release to the given plugin list + * + * @param pluginName the name of the plugin + * @param pluginsListFile The plugin list file + * @param makeLatest Whether to make the release the latest release + * + * @return the updated plugin list + */ + GPathResult publishRelease(String pluginName, Resource pluginsList, boolean makeLatest) { + def xml = parsePluginList(pluginsList) + + xml.@revision = revision + + def releaseMetadata = getPluginMetadata(pluginName) + def pluginVersion = releaseMetadata.@version.toString().trim() + def releaseTag = "RELEASE_${pluginVersion.replaceAll('\\.','_')}" + + def props = ['title', 'author', 'authorEmail', 'description', 'documentation'] + def releaseInfo = { + release([tag:releaseTag,version:pluginVersion, type:'svn']) { + for(p in props) { + "$p"(releaseMetadata."$p".text()) + } + } + } + + // build argument, if makeLatest is true make the plugin the latest release + def pluginArgs = [name:pluginName] + if(makeLatest) { + pluginArgs.'latest-release' = pluginVersion + } + + def pluginInfo = { + plugin(pluginArgs, releaseInfo) + } + + + // find plugin + def allPlugins = xml.plugin + if(allPlugins.size()==0) { + // create new plugin list + xml << pluginInfo + } + else { + def existingEntry = xml.plugin.find { it.@name == pluginName } + + if(existingEntry.size()==0) { + // plugin doesn't exist, create new entry + def lastPlugin = allPlugins[allPlugins.size()-1] + lastPlugin + pluginInfo + } + else { + // plugin exists, add release info and make latest is appropriate + if(makeLatest) { + existingEntry.'@latest-release' = pluginVersion + } + def existingRelease = existingEntry.release.find { it.@version == pluginVersion } + if(existingRelease.size()==0) { + existingEntry << releaseInfo + } + } + } + + return xml + + } + + protected GPathResult parsePluginList(Resource pluginsListFile) { + if(pluginsListFile.exists()) { + InputStream stream = pluginsListFile.getInputStream() + try { + return new XmlSlurper().parse(stream) + } + finally { + stream?.close() + } + } + else { + return new XmlSlurper().parseText('') + } + } + + GPathResult publishRelease(String pluginName, Resource pluginsList) { + publishRelease(pluginName, pluginsList, true) + } + + protected GPathResult getPluginMetadata(String pluginName) { + def basedir = BuildSettingsHolder.settings?.baseDir ?: new File(".") + return new XmlSlurper().parse(new File("${basedir.absolutePath}/plugin.xml")) + } +} \ No newline at end of file diff --git a/grails/src/test/org/codehaus/groovy/grails/plugins/publishing/DefaultPluginPublisherTests.groovy b/grails/src/test/org/codehaus/groovy/grails/plugins/publishing/DefaultPluginPublisherTests.groovy new file mode 100644 index 000000000..d3ae0fa77 --- /dev/null +++ b/grails/src/test/org/codehaus/groovy/grails/plugins/publishing/DefaultPluginPublisherTests.groovy @@ -0,0 +1,273 @@ +package org.codehaus.groovy.grails.plugins.publishing + +import groovy.util.slurpersupport.GPathResult +import org.springframework.core.io.ByteArrayResource +import org.springframework.core.io.Resource + +/** + * @author Graeme Rocher + * @since 1.2 + */ + +public class DefaultPluginPublisherTests extends GroovyTestCase{ + void testPublishNewPluginRelease() { + def publisher = new TestPluginPublisher() + publisher.testPluginsXml = '''\ + + + + + Plugin summary/headline + Your name + + \ +Brief description of the plugin. + + http://grails.org/Test1+Plugin + file:///Developer/localsvn/grails-test1/tags/RELEASE_0_1/grails-test1-0.1.zip + + + + + Plugin summary/headline + Your name + + \ +Brief description of the plugin. + + http://grails.org/Test1+Plugin + file:///Developer/localsvn/grails-test1/tags/RELEASE_0_1/grails-test1-0.1.zip + + + +''' + publisher.testMetadata = '''\ + + Bob + FooBar Plugin + some text + http://grails.org/plugin/foo-bar + + DataSource + UrlMappings + + + + +''' + + + + def result = publisher.publishRelease("foo-bar", new ByteArrayResource("".bytes)) + + def writer = new StringWriter() + writer << new groovy.xml.StreamingMarkupBuilder().bind { + mkp.yield result + } + new XmlNodePrinter().print(new XmlParser().parseText(writer.toString())) + result = new XmlSlurper().parseText(writer.toString()) + + assertEquals 3, result.plugin.size() + + def testPlugin = result.plugin.find { it.@name == 'foo-bar' } + + assertEquals 'foo-bar', testPlugin.@name.text() + assertEquals '0.1', testPlugin.'@latest-release'.text() + def releaseInfo = testPlugin.release + + assertEquals 'RELEASE_0_1', releaseInfo.@tag.text() + assertEquals '0.1', releaseInfo.@version.text() + assertEquals 'Bob', releaseInfo.author.text() + assertEquals 'FooBar Plugin', releaseInfo.title.text() + } + + void testPublishExistingPluginRelease() { + def publisher = new TestPluginPublisher() + publisher.testPluginsXml = '''\ + + + + + FooBar Plugin + Bob + + + http://grails.org/Test1+Plugin + file:///Developer/localsvn/grails-test1/tags/RELEASE_0_1/grails-test1-0.1.zip + + + +''' + publisher.testMetadata = '''\ + + Bob + FooBar Plugin + some text + http://grails.org/plugin/foo-bar + + DataSource + UrlMappings + + + + + ''' + + def result = publisher.publishRelease("foo-bar", new ByteArrayResource("".bytes)) + + def writer = new StringWriter() + writer << new groovy.xml.StreamingMarkupBuilder().bind { + mkp.yield result + } + new XmlNodePrinter().print(new XmlParser().parseText(writer.toString())) + result = new XmlSlurper().parseText(writer.toString()) + + assertEquals 1, result.plugin.size() + + def testPlugin = result.plugin.find { it.@name == 'foo-bar' } + + assertEquals 'foo-bar', testPlugin.@name.text() + assertEquals '0.2', testPlugin.'@latest-release'.text() + def releaseInfo = testPlugin.release + + assertEquals 2, releaseInfo.size() + + assertEquals 'RELEASE_0_1', releaseInfo[0].@tag.text() + assertEquals '0.1', releaseInfo[0].@version.text() + assertEquals 'Bob', releaseInfo[0].author.text() + assertEquals 'FooBar Plugin', releaseInfo[0].title.text() + + assertEquals 'RELEASE_0_2', releaseInfo[1].@tag.text() + assertEquals '0.2', releaseInfo[1].@version.text() + assertEquals 'Bob', releaseInfo[1].author.text() + assertEquals 'FooBar Plugin', releaseInfo[1].title.text() + + } + + + void testPublishExistingPluginReleaseDontMakeLatest() { + def publisher = new TestPluginPublisher() + publisher.testPluginsXml = '''\ + + + + + FooBar Plugin + Bob + + + http://grails.org/Test1+Plugin + file:///Developer/localsvn/grails-test1/tags/RELEASE_0_1/grails-test1-0.1.zip + + + +''' + publisher.testMetadata = '''\ + + Bob + FooBar Plugin + some text + http://grails.org/plugin/foo-bar + + DataSource + UrlMappings + + + + +''' + + def result = publisher.publishRelease("foo-bar", new ByteArrayResource("".bytes),false) + + def writer = new StringWriter() + writer << new groovy.xml.StreamingMarkupBuilder().bind { + mkp.yield result + } + new XmlNodePrinter().print(new XmlParser().parseText(writer.toString())) + result = new XmlSlurper().parseText(writer.toString()) + + assertEquals 1, result.plugin.size() + + def testPlugin = result.plugin.find { it.@name == 'foo-bar' } + + assertEquals 'foo-bar', testPlugin.@name.text() + assertEquals '0.1', testPlugin.'@latest-release'.text() + def releaseInfo = testPlugin.release + + assertEquals 2, releaseInfo.size() + + assertEquals 'RELEASE_0_1', releaseInfo[0].@tag.text() + assertEquals '0.1', releaseInfo[0].@version.text() + assertEquals 'Bob', releaseInfo[0].author.text() + assertEquals 'FooBar Plugin', releaseInfo[0].title.text() + + assertEquals 'RELEASE_0_2', releaseInfo[1].@tag.text() + assertEquals '0.2', releaseInfo[1].@version.text() + assertEquals 'Bob', releaseInfo[1].author.text() + assertEquals 'FooBar Plugin', releaseInfo[1].title.text() + + } + + void testPublishFirstPluginRelease() { + def publisher = new TestPluginPublisher() + publisher.testPluginsXml = '' + publisher.testMetadata = '''\ + + Bob + FooBar Plugin + some text + http://grails.org/plugin/foo-bar + + DataSource + UrlMappings + + + + + ''' + + + + def result = publisher.publishRelease("foo-bar", new ByteArrayResource("".bytes)) + + def writer = new StringWriter() + writer << new groovy.xml.StreamingMarkupBuilder().bind { + mkp.yield result + } + new XmlNodePrinter().print(new XmlParser().parseText(writer.toString())) + result = new XmlSlurper().parseText(writer.toString()) + + assertEquals 1, result.plugin.size() + + def testPlugin = result.plugin.find { it.@name == 'foo-bar' } + + assertEquals 'foo-bar', testPlugin.@name.text() + assertEquals '0.1', testPlugin.'@latest-release'.text() + def releaseInfo = testPlugin.release + + assertEquals 'RELEASE_0_1', releaseInfo.@tag.text() + assertEquals '0.1', releaseInfo.@version.text() + assertEquals 'Bob', releaseInfo.author.text() + assertEquals 'FooBar Plugin', releaseInfo.title.text() + + } +} + +class TestPluginPublisher extends DefaultPluginPublisher{ + String testPluginsXml + String testMetadata + + public TestPluginPublisher(String revNumber) { + super("0"); + } + + protected GPathResult getPluginMetadata(String pluginName) { + new XmlSlurper().parseText(testMetadata) + } + + public GPathResult parsePluginList(Resource pluginsListFile) { + new XmlSlurper().parseText(testPluginsXml) + } + + +} \ No newline at end of file