diff --git a/pom.xml b/pom.xml index c38fd8017e..d0bdd19991 100644 --- a/pom.xml +++ b/pom.xml @@ -343,6 +343,11 @@ 2.1 runtime + + org.apache.maven + maven-artifact + 3.3.9 + diff --git a/src/main/java/com/rultor/agents/github/qtn/QnRelease.java b/src/main/java/com/rultor/agents/github/qtn/QnRelease.java index 396f47ffbb..cb9568337b 100644 --- a/src/main/java/com/rultor/agents/github/qtn/QnRelease.java +++ b/src/main/java/com/rultor/agents/github/qtn/QnRelease.java @@ -40,6 +40,8 @@ import java.io.IOException; import java.net.URI; import java.util.ResourceBundle; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -49,7 +51,6 @@ * @author Yegor Bugayenko (yegor@teamed.io) * @version $Id$ * @since 1.3.6 - * @checkstyle MultipleStringLiteralsCheck (500 lines) */ @Immutable @ToString @@ -65,31 +66,56 @@ public final class QnRelease implements Question { @Override public Req understand(final Comment.Smart comment, final URI home) throws IOException { - new Answer(comment).post( - true, - String.format( - QnRelease.PHRASES.getString("QnRelease.start"), - home.toASCIIString() - ) - ); final Issue issue = comment.issue(); Logger.info( this, "release request found in %s#%d, comment #%d", issue.repo().coordinates(), issue.number(), comment.number() ); - return new Req.Simple( - "release", - new ImmutableMap.Builder() - .put("head_branch", "master") - .put( - "head", + final Req req; + final Matcher matcher = Pattern.compile(".*release.*`(.+)`.*") + .matcher(comment.body()); + if (matcher.matches()) { + final String name = matcher.group(1); + final ReleaseTag release = new ReleaseTag(issue.repo(), name); + if (release.release()) { + new Answer(comment).post( + true, String.format( - "git@github.com:%s.git", - comment.issue().repo().coordinates() + QnRelease.PHRASES.getString("QnRelease.start"), + home.toASCIIString() ) - ) - .build() - ); + ); + req = new Req.Simple( + "release", + new ImmutableMap.Builder() + .put("head_branch", "master") + .put( + "head", + String.format( + "git@github.com:%s.git", + comment.issue().repo().coordinates() + ) + ).build() + ); + } else { + new Answer(comment).post( + false, + String.format( + QnRelease.PHRASES.getString("QnRelease.invalid-tag"), + name, + release.reference() + ) + ); + req = Req.EMPTY; + } + } else { + new Answer(comment).post( + false, + QnRelease.PHRASES.getString("QnRelease.missing-tag") + ); + req = Req.EMPTY; + } + return req; } } diff --git a/src/main/java/com/rultor/agents/github/qtn/ReleaseTag.java b/src/main/java/com/rultor/agents/github/qtn/ReleaseTag.java new file mode 100644 index 0000000000..6e51fcf0f0 --- /dev/null +++ b/src/main/java/com/rultor/agents/github/qtn/ReleaseTag.java @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2009-2015, rultor.com + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: 1) Redistributions of source code must retain the above + * copyright notice, this list of conditions and the following + * disclaimer. 2) Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. 3) Neither the name of the rultor.com nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.rultor.agents.github.qtn; + +import com.jcabi.aspects.Immutable; +import com.jcabi.github.Release; +import com.jcabi.github.Repo; +import com.jcabi.github.Smarts; +import java.io.IOException; +import java.util.regex.Pattern; +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; + +/** + * Release Tag validator, ensures not releasing already outdated tags. + * + * @author Armin Braun (me@obrown.io) + * @version $Id$ + * @since 1.62 + */ +@Immutable +final class ReleaseTag { + + /** + * Pattern matching semantically valid versions. + */ + private static final Pattern VERSION_PATTERN = + Pattern.compile("^(\\d+\\.)*(\\d+)$"); + + /** + * Github. + */ + private final transient Repo repo; + + /** + * Tag-name of the release. + */ + private final transient String name; + + /** + * Ctor. + * @param rpo Github repo + * @param version String release tag name + */ + ReleaseTag(final Repo rpo, final String version) { + this.repo = rpo; + this.name = version; + } + + /** + * Checks if this tag can be released. + * A tag can be released if it is either not named as a semantically + * correct version or has a higher version number than all existing tags. + * @return Boolean + * @throws IOException on error. + */ + public boolean release() throws IOException { + return !ReleaseTag.valid(this.name) + || ReleaseTag.newer(this.reference(), this.name); + } + + /** + * Returns the tag name of the highest version from the repo. + * @return String name of the highest released version. + * @throws IOException on error. + */ + public String reference() throws IOException { + String tag = "0"; + final Iterable rels = + new Smarts<>(this.repo.releases().iterate()); + for (final Release.Smart rel : rels) { + final String version = rel.tag(); + if (ReleaseTag.valid(version) && ReleaseTag.newer(tag, version)) { + tag = version; + } + } + return tag; + } + + /** + * Checks that a tag is newer than a given reference. + * @param reference String + * @param tag String + * @return Boolean true if tag is new than reference. + */ + private static boolean newer(final String reference, final String tag) { + return new DefaultArtifactVersion(reference).compareTo( + new DefaultArtifactVersion(tag) + ) < 0; + } + + /** + * Checks tag string format being a valid release version. + * @param identifier String tag name. + * @return Boolean + */ + private static boolean valid(final String identifier) { + return ReleaseTag.VERSION_PATTERN.matcher(identifier).matches(); + } + +} diff --git a/src/main/resources/phrases_en_US.properties b/src/main/resources/phrases_en_US.properties index bec76ee393..640ca5e825 100644 --- a/src/main/resources/phrases_en_US.properties +++ b/src/main/resources/phrases_en_US.properties @@ -8,7 +8,12 @@ QnMerge.base-is-gone=Base repository is gone, can't merge into it QnDeploy.start=OK, I'll try to deploy now. You can check the progress [here](%s) QnRelease.start=OK, I will release it now. Please check the progress [here](%s) - +QnRelease.missing-tag=No release tag specified, please provide a release version.\n \ + More information on the release command can be found \ + [here](http://doc.rultor.com/basics.html). +QnRelease.invalid-tag=Invalid release tag `%s` specified. There is already a \ + release `%s` newer than the given release in this \ + repository. QnStop.stop=OK, I'll try to stop current task. QnAskedBy.denied=Sorry, I accept such requests only from authorized commanders: %s \ diff --git a/src/test/java/com/rultor/agents/github/qtn/QnReleaseTest.java b/src/test/java/com/rultor/agents/github/qtn/QnReleaseTest.java index 0af4a090f1..c925bf18a8 100644 --- a/src/test/java/com/rultor/agents/github/qtn/QnReleaseTest.java +++ b/src/test/java/com/rultor/agents/github/qtn/QnReleaseTest.java @@ -34,8 +34,10 @@ import com.jcabi.github.Repo; import com.jcabi.github.mock.MkGithub; import com.jcabi.matchers.XhtmlMatchers; +import com.rultor.agents.github.Req; import java.net.URI; import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; import org.junit.Test; import org.xembly.Directives; import org.xembly.Xembler; @@ -59,7 +61,7 @@ public final class QnReleaseTest { public void buildsRequest() throws Exception { final Repo repo = new MkGithub().randomRepo(); final Issue issue = repo.issues().create("", ""); - issue.comments().post("release"); + issue.comments().post("release `1.7`"); MatcherAssert.assertThat( new Xembler( new Directives().add("request").append( @@ -77,4 +79,48 @@ public void buildsRequest() throws Exception { ); } + /** + * QnRelease can deny release when tag is outdated. + * @throws Exception In case of error. + */ + @Test + public void denyOutdatedTag() throws Exception { + final Repo repo = new MkGithub().randomRepo(); + final Issue issue = repo.issues().create("", ""); + repo.releases().create("1.7"); + issue.comments().post("release `1.6`"); + MatcherAssert.assertThat( + new QnRelease().understand( + new Comment.Smart(issue.comments().get(1)), new URI("#") + ), + Matchers.is(Req.EMPTY) + ); + MatcherAssert.assertThat( + new Comment.Smart(issue.comments().get(2)).body(), + Matchers.containsString("There is already a release `1.7`") + ); + issue.comments().post("release"); + } + + /** + * QnRelease can deny release when tag name is not given. + * @throws Exception In case of error. + */ + @Test + public void denyMissingTag() throws Exception { + final Issue issue = new MkGithub().randomRepo().issues() + .create("", ""); + issue.comments().post("release"); + MatcherAssert.assertThat( + new QnRelease().understand( + new Comment.Smart(issue.comments().get(1)), new URI("#") + ), + Matchers.is(Req.EMPTY) + ); + MatcherAssert.assertThat( + new Comment.Smart(issue.comments().get(2)).body(), + Matchers.containsString("No release tag specified") + ); + } + } diff --git a/src/test/java/com/rultor/agents/github/qtn/ReleaseTagTest.java b/src/test/java/com/rultor/agents/github/qtn/ReleaseTagTest.java new file mode 100644 index 0000000000..ce0eb78701 --- /dev/null +++ b/src/test/java/com/rultor/agents/github/qtn/ReleaseTagTest.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2009-2015, rultor.com + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: 1) Redistributions of source code must retain the above + * copyright notice, this list of conditions and the following + * disclaimer. 2) Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. 3) Neither the name of the rultor.com nor + * the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT + * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.rultor.agents.github.qtn; + +import com.jcabi.github.Repo; +import com.jcabi.github.mock.MkGithub; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Test; + +/** + * Tests for ${@link ReleaseTag}. + * + * @author Armin Braun (me@obrown.io) + * @version $Id$ + * @since 1.62 + */ +public final class ReleaseTagTest { + + /** + * ReleaseTag can deny release for outdated, semantically correct versions. + * @throws Exception In case of error. + */ + @Test + public void validatesReleaseVersion() throws Exception { + final Repo repo = new MkGithub().randomRepo(); + repo.releases().create("1.74"); + MatcherAssert.assertThat( + new ReleaseTag(repo, "1.87.15").release(), + Matchers.is(true) + ); + MatcherAssert.assertThat( + new ReleaseTag(repo, "1.5-bar").release(), + Matchers.is(true) + ); + MatcherAssert.assertThat( + new ReleaseTag(repo, "1.9-beta").release(), + Matchers.is(true) + ); + MatcherAssert.assertThat( + new ReleaseTag(repo, "1.62").release(), + Matchers.is(false) + ); + } + + /** + * ReleaseTag can retrieve the latest release version in the repo. + * @throws Exception In case of error. + */ + @Test + public void getsReferenceVersion() throws Exception { + final Repo repo = new MkGithub().randomRepo(); + final String latest = "2.2.1"; + repo.releases().create("1.0"); + repo.releases().create(latest); + repo.releases().create("3.0-beta"); + MatcherAssert.assertThat( + new ReleaseTag(repo, "2.4").reference(), + Matchers.is(latest) + ); + } +}