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)
+ );
+ }
+}