diff --git a/.travis.yml b/.travis.yml index 46bb9ac..282ab9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: groovy jdk: -- oraclejdk8 +- openjdk8 before_script: unset CI after_script: set CI=true diff --git a/README.md b/README.md index a9db291..ebebdbd 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ Say Good Bye to Temporary Hacks # Introduction +## @Remember + `@Remember` is an annotation which helps you not to forget any temporary solution (aka hacks or quick wins) you have introduced into your code base. You specify the date in the future when you want to revisit the code, e.g. `@Remember('2018-12-24)`. After this date the code no longer compiles forcing you to re-evaluate if the code is still required or to find more permanent solution. -## Full Usage - ```groovy import com.agorapulse.remember.Remember @@ -35,6 +35,21 @@ You can add an `owner` who is responsible for action which needs to be taken whe You can force failing on continuous integration server by setting `ci` to `true`. By default, the annotation will only fail during local builds. +## @DoNotMerge + +If you have a code you want to discuss with your colleagues before merging to the main branch or simply +you have created a temporary solution which should never enter the main branch then you can use `@DoNotMerge` +annotation. If the library recognizes (on a best effort) that the build has been triggered by pull request then +it will fail to compile. + +```groovy +import com.agorapulse.remember.DoNotMerge + +@DoNotMerge('Just testing some stuff') +class Subject { } +``` + + ## Maintained by [![Agorapulse](https://cloud.githubusercontent.com/assets/139017/17053391/4a44735a-5034-11e6-8e72-9f4b7139d7e0.png)](https://www.agorapulse.com/) diff --git a/build.gradle b/build.gradle index cef73bd..63fa62d 100644 --- a/build.gradle +++ b/build.gradle @@ -9,10 +9,11 @@ repositories { dependencies { compile 'org.codehaus.groovy:groovy-all:2.4.14' + testCompile 'com.github.stefanbirkner:system-rules:1.19.0' testCompile 'org.spockframework:spock-core:1.1-groovy-2.4' } -version '0.2' +version '0.3' group 'com.agorapulse' diff --git a/gradlew.bat b/gradlew.bat old mode 100644 new mode 100755 diff --git a/src/main/groovy/com/agorapulse/remember/DoNotMerge.java b/src/main/groovy/com/agorapulse/remember/DoNotMerge.java new file mode 100644 index 0000000..dad3fd3 --- /dev/null +++ b/src/main/groovy/com/agorapulse/remember/DoNotMerge.java @@ -0,0 +1,34 @@ +package com.agorapulse.remember; + +import org.codehaus.groovy.transform.GroovyASTTransformationClass; + +import java.lang.annotation.*; + +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target({ + ElementType.TYPE, + ElementType.FIELD, + ElementType.METHOD, + ElementType.PARAMETER, + ElementType.CONSTRUCTOR, + ElementType.LOCAL_VARIABLE, + ElementType.ANNOTATION_TYPE, + ElementType.PACKAGE, + ElementType.TYPE_PARAMETER, + ElementType.TYPE_USE +}) +@GroovyASTTransformationClass("com.agorapulse.remember.DoNotMergeTransformation") +/** + * @DoNotMergeis an annotation which helps you not to forget any temporary solution on a feature branch + * which you have introduced into your code base. The code won't compile if the code is running from pull request continuous build. + * + */ +public @interface DoNotMerge { + + /** + * @return description why should the code expression should not be merged into the main branch + */ + String value() default "Do not merge"; + +} diff --git a/src/main/groovy/com/agorapulse/remember/DoNotMergeTransformation.java b/src/main/groovy/com/agorapulse/remember/DoNotMergeTransformation.java new file mode 100644 index 0000000..d5e1884 --- /dev/null +++ b/src/main/groovy/com/agorapulse/remember/DoNotMergeTransformation.java @@ -0,0 +1,51 @@ +package com.agorapulse.remember; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.syntax.SyntaxException; +import org.codehaus.groovy.transform.ASTTransformation; +import org.codehaus.groovy.transform.GroovyASTTransformation; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Optional; + +/** + * AST Transformation for {@link DoNotMerge} annotation. + */ +@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS) +public class DoNotMergeTransformation implements ASTTransformation { + + @Override + public void visit(ASTNode[] nodes, SourceUnit source) { + AnnotationNode annotation = (AnnotationNode) nodes[0]; + + Expression valueExpression = annotation.getMember("value"); + String value = Optional.ofNullable(valueExpression).map(Expression::getText).orElse("Do not merge"); + + if (isPullRequest()) { + source.addError(createSyntaxException(annotation, value)); + } + } + + private boolean isPullRequest() { + return System.getenv() + .keySet() + .stream() + .filter(key -> key.endsWith("PULL_REQUEST")) + .findAny() + .map(System::getenv) + .map(value -> value.length() > 0 && !"false".equals(value)) + .orElse(false); + + } + + private SyntaxException createSyntaxException(AnnotationNode annotation, String message) { + return new SyntaxException(message, annotation.getLineNumber(), annotation.getColumnNumber(), annotation.getColumnNumber(), annotation.getLastColumnNumber()); + } +} diff --git a/src/test/groovy/com/agorapulse/remember/DoNotMergeSpec.groovy b/src/test/groovy/com/agorapulse/remember/DoNotMergeSpec.groovy new file mode 100644 index 0000000..59de110 --- /dev/null +++ b/src/test/groovy/com/agorapulse/remember/DoNotMergeSpec.groovy @@ -0,0 +1,98 @@ +package com.agorapulse.remember + +import groovy.test.GroovyAssert +import org.codehaus.groovy.control.MultipleCompilationErrorsException +import org.codehaus.groovy.control.messages.SyntaxErrorMessage +import org.codehaus.groovy.syntax.SyntaxException +import org.junit.Rule +import org.junit.contrib.java.lang.system.EnvironmentVariables +import spock.lang.Specification +import spock.util.environment.RestoreSystemProperties + +class DoNotMergeSpec extends Specification { + + public static final String PR_ENV_VAR_NAME = 'TRAVIS_PULL_REQUEST' + @Rule EnvironmentVariables environmentVariables = new EnvironmentVariables() + + void 'the annotation is ignored by default'() { + when: + // to pass PR build for this library + environmentVariables.clear(PR_ENV_VAR_NAME) + + // language=Groovy + GroovyAssert.assertScript """ + import com.agorapulse.remember.DoNotMerge + + @DoNotMerge + class Subject { } + + true + """ + then: + noExceptionThrown() + } + + void 'the annotation is ignored of the pull request env var is false'() { + when: + // to pass PR build for this library + environmentVariables.set(PR_ENV_VAR_NAME, 'false') + + // language=Groovy + GroovyAssert.assertScript """ + import com.agorapulse.remember.DoNotMerge + + @DoNotMerge + class Subject { } + + true + """ + then: + noExceptionThrown() + } + + void 'error is reported on PR build'() { + when: + environmentVariables.set(PR_ENV_VAR_NAME, '123456') + // language=Groovy + GroovyAssert.assertScript """ + import com.agorapulse.remember.DoNotMerge + + @DoNotMerge + class Subject { } + + true + """ + then: + MultipleCompilationErrorsException e = thrown(MultipleCompilationErrorsException) + assertMessage(e, 'Do not merge @ line 4, column 17.') + } + + void 'error is reported on PR build - with details'() { + when: + environmentVariables.set(PR_ENV_VAR_NAME, '123456') + // language=Groovy + GroovyAssert.assertScript """ + import com.agorapulse.remember.DoNotMerge + + @DoNotMerge('This will break everything!') + class Subject { } + + true + """ + then: + MultipleCompilationErrorsException e = thrown(MultipleCompilationErrorsException) + assertMessage(e, 'This will break everything! @ line 4, column 17.') + } + + boolean assertMessage(MultipleCompilationErrorsException multipleCompilationErrorsException, String message) { + assert multipleCompilationErrorsException.errorCollector + assert multipleCompilationErrorsException.errorCollector.errorCount == 1 + assert multipleCompilationErrorsException.errorCollector.errors.first() instanceof SyntaxErrorMessage + + SyntaxException exception = multipleCompilationErrorsException.errorCollector.errors.first().cause + assert exception.message == message + + return true + } + +}