Repository: vackosar/gitflow-incremental-builder
Pull request #109 (2020-01-12 18:37:07)

Issue #108: property short names, auto-mapping to "true" and new "help" property


Patch:
diff --git a/.gitignore b/.gitignore
index d2f012b..a9e577d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,5 +9,5 @@
 .classpath
 .project
 .settings/
-.README.md.html
+.*.md.html
 *.log
diff --git a/README.md b/README.md
index 4d42bb4..7ea95d4 100644
--- a/README.md
+++ b/README.md
@@ -247,29 +247,44 @@ Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
 Maven pom properties configuration with default values is below:
 ```xml
 <properties>
-    <gib.enabled>true</gib.enabled>
-    <gib.disableBranchComparison>false</gib.disableBranchComparison>
-    <gib.referenceBranch>refs/remotes/origin/develop</gib.referenceBranch>
-    <gib.fetchReferenceBranch>false</gib.fetchReferenceBranch>
-    <gib.baseBranch>HEAD</gib.baseBranch>
-    <gib.fetchBaseBranch>false</gib.fetchBaseBranch>
-    <gib.compareToMergeBase>true</gib.compareToMergeBase>
-    <gib.uncommited>true</gib.uncommited>
-    <gib.untracked>true</gib.untracked>
-    <gib.excludePathRegex>(?!x)x</gib.excludePathRegex>
-    <gib.buildAll>false</gib.buildAll>
-    <gib.buildDownstream>always</gib.buildDownstream>
-    <gib.buildUpstream>derived</gib.buildUpstream>
-    <gib.buildUpstreamMode>changed</gib.buildUpstreamMode>
-    <gib.skipTestsForUpstreamModules>false</gib.skipTestsForUpstreamModules>
-    <gib.argsForUpstreamModules></gib.argsForUpstreamModules>
-    <gib.forceBuildModules></gib.forceBuildModules>
-    <gib.excludeTransitiveModulesPackagedAs></gib.excludeTransitiveModulesPackagedAs>
-    <gib.failOnMissingGitDir>true</gib.failOnMissingGitDir>
-    <gib.failOnError>true</gib.failOnError>
+    <gib.help>false</gib.help>                                                            <!-- or <gib.h>... -->
+    <gib.enabled>true</gib.enabled>                                                       <!-- or <gib.e>... -->
+    <gib.disableBranchComparison>false</gib.disableBranchComparison>                      <!-- or <gib.dbc>... -->
+    <gib.referenceBranch>refs/remotes/origin/develop</gib.referenceBranch>                <!-- or <gib.rb>... -->
+    <gib.fetchReferenceBranch>false</gib.fetchReferenceBranch>                            <!-- or <gib.frb>... -->
+    <gib.baseBranch>HEAD</gib.baseBranch>                                                 <!-- or <gib.bb>... -->
+    <gib.fetchBaseBranch>false</gib.fetchBaseBranch>                                      <!-- or <gib.fbb>... -->
+    <gib.compareToMergeBase>true</gib.compareToMergeBase>                                 <!-- or <gib.ctmb>... -->
+    <gib.uncommited>true</gib.uncommited>                                                 <!-- or <gib.uc>... -->
+    <gib.untracked>true</gib.untracked>                                                   <!-- or <gib.ut>... -->
+    <gib.excludePathRegex>(?!x)x</gib.excludePathRegex>                                   <!-- or <gib.epr>... -->
+    <gib.buildAll>false</gib.buildAll>                                                    <!-- or <gib.ba>... -->
+    <gib.buildDownstream>always</gib.buildDownstream>                                     <!-- or <gib.bd>... -->
+    <gib.buildUpstream>derived</gib.buildUpstream>                                        <!-- or <gib.bu>... -->
+    <gib.buildUpstreamMode>changed</gib.buildUpstreamMode>                                <!-- or <gib.bum>... -->
+    <gib.skipTestsForUpstreamModules>false</gib.skipTestsForUpstreamModules>              <!-- or <gib.stfum>... -->
+    <gib.argsForUpstreamModules></gib.argsForUpstreamModules>                             <!-- or <gib.afum>... -->
+    <gib.forceBuildModules></gib.forceBuildModules>                                       <!-- or <gib.fbm>... -->
+    <gib.excludeTransitiveModulesPackagedAs></gib.excludeTransitiveModulesPackagedAs>     <!-- or <gib.etmpa>... -->
+    <gib.failOnMissingGitDir>true</gib.failOnMissingGitDir>                               <!-- or <gib.fomgd>... -->
+    <gib.failOnError>true</gib.failOnError>                                               <!-- or <gib.foe>... -->
 </properties>
 ```
 
+Each property can also be set via `-D` on the command line and to reduce typing to a minimum, each property has a short name (see code block above).
+
+E.g. `-Dgib.e=true` yields the same result as `-Dgib.enabled=true`.
+
+Properties that support the value `true` can be specified _without_ a value, e.g. `-Dgib.enabled` is the same as `-Dgib.enabled=true`.
+
+System properties (`-D`) take precedence over project properties from the POM and secondarily to that a full name takes precedence over the respective short name.
+
+### gib.help
+
+Logs the available properties etc.
+
+Note: Independent of `gib.enabled`.
+
 ### gib.enabled
 
 Can be used to disable this extension temporarily or permanently (e.g. to avoid clashes with IDE building strategy).
diff --git a/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/Configuration.java b/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/Configuration.java
index 6cc4922..e89915c 100644
--- a/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/Configuration.java
+++ b/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/Configuration.java
@@ -15,7 +15,6 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.Properties;
 import java.util.Set;
@@ -54,7 +53,7 @@ public class Configuration {
     public final boolean failOnError;
 
     private Configuration(MavenSession session) {
-        Properties projectProperties = session.getTopLevelProject().getProperties();
+        Properties projectProperties = getProjectProperties(session);
         checkProperties(projectProperties);
 
         // change detection config
@@ -100,13 +99,27 @@ public class Configuration {
      * @return whether or not GIB is enabled or not
      */
     public static boolean isEnabled(MavenSession session) {
-        return Boolean.valueOf(Property.enabled.getValue(session.getTopLevelProject().getProperties()));
+        return Boolean.valueOf(Property.enabled.getValue(getProjectProperties(session)));
+    }
+
+    /**
+     * Returns the value for {@link Property#help} without initializing all the other configuration fields (help can be requested even if
+     * {@link #isEnabled(MavenSession)} returns {@code false}).
+     *
+     * @param session the current session
+     * @return whether or not to print GIB help
+     */
+    public static boolean isHelpRequested(MavenSession session) {
+        return Boolean.valueOf(Property.help.getValue(getProjectProperties(session)));
+    }
+
+    private static Properties getProjectProperties(MavenSession session) {
+        return session.getTopLevelProject().getProperties();
     }
 
     private static void checkProperties(Properties projectProperties) {
         Set<String> availablePropertyNames = Arrays.stream(Property.values())
-                .flatMap(p -> Stream.of(p.fullName(), p.deprecatedFullName()))
-                .filter(Objects::nonNull)
+                .flatMap(p -> p.allNames().stream())
                 .collect(Collectors.toSet());
         String invalidPropertyNames = Stream.concat(System.getProperties().keySet().stream(), projectProperties.keySet().stream())
                 .distinct()
@@ -127,7 +140,7 @@ public class Configuration {
             String propertyValue = Optional.ofNullable(Property.buildUpstreamMode.getValue(projectProperties)).orElse("");
             return BuildUpstreamMode.valueOf(propertyValue.toUpperCase());
         } catch (IllegalArgumentException e) {
-            throw new IllegalArgumentException("GIB property " + Property.buildUpstreamMode.fullName() + " defines an invalid mode", e);
+            throw new IllegalArgumentException("GIB property " + Property.buildUpstreamMode.fullOrShortName() + " defines an invalid mode", e);
         }
     }
 
@@ -144,7 +157,7 @@ public class Configuration {
                 return false;
             default:
                 throw new IllegalArgumentException(
-                        "GIB property " + property.fullName() + " defines an invalid value: " + property.getValue(projectProperties));
+                        "GIB property " + property.fullOrShortName() + " defines an invalid value: " + property.getValue(projectProperties));
         }
     }
 
@@ -171,7 +184,7 @@ public class Configuration {
         try {
             return Pattern.compile(patternString);
         } catch (PatternSyntaxException e) {
-            throw new IllegalArgumentException("GIB property " + property.fullName() + " defines an invalid pattern string", e);
+            throw new IllegalArgumentException("GIB property " + property.fullOrShortName() + " defines an invalid pattern string", e);
         }
     }
 
diff --git a/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/MavenLifecycleParticipant.java b/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/MavenLifecycleParticipant.java
index 493ce25..066d1f9 100644
--- a/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/MavenLifecycleParticipant.java
+++ b/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/MavenLifecycleParticipant.java
@@ -22,9 +22,24 @@ public class MavenLifecycleParticipant extends AbstractMavenLifecycleParticipant
 
     @Inject private Configuration.Provider configProvider;
 
+    private final String implVersion;
+
+    public MavenLifecycleParticipant() {
+        implVersion = getClass().getPackage().getImplementationVersion();
+    }
+
+    // only for testing!
+    MavenLifecycleParticipant(String implVersion) {
+        this.implVersion = implVersion;
+    }
+
     @Override
     public void afterProjectsRead(MavenSession session) throws MavenExecutionException {
 
+        if (Configuration.isHelpRequested(session)) {
+            logHelp();
+        }
+
         if (!Configuration.isEnabled(session)) {
             logger.info("gitflow-incremental-builder is disabled.");
             return;
@@ -34,11 +49,11 @@ public class MavenLifecycleParticipant extends AbstractMavenLifecycleParticipant
         if (session.getProjectDependencyGraph() == null) {
             logger.warn("Execution of gitflow-incremental-builder is not supported in this environment: "
                     + "Current MavenSession does not provide a ProjectDependencyGraph. "
-                    + "Consider disabling gitflow-incremental-builder via property: " + Property.enabled.fullName());
+                    + "Consider disabling gitflow-incremental-builder via property: " + Property.enabled.fullOrShortName());
             return;
         }
 
-        logger.info("gitflow-incremental-builder " + getClass().getPackage().getImplementationVersion() + " starting...");
+        logger.info("gitflow-incremental-builder {} starting...", implVersion);
         try {
             unchangedProjectsRemover.act();
         } catch (Exception e) {
@@ -52,4 +67,12 @@ public class MavenLifecycleParticipant extends AbstractMavenLifecycleParticipant
         }
         logger.info("gitflow-incremental-builder exiting...");
     }
+
+    private void logHelp() {
+        logger.info("gitflow-incremental-builder {} help:\n{}\nFor more help see: {}/tree/version/{}#configuration\n",
+                implVersion,
+                Property.exemplifyAll(),
+                "https://github.com/vackosar/gitflow-incremental-builder",
+                implVersion);
+    }
 }
diff --git a/src/main/java/com/vackosar/gitflowincrementalbuild/control/Property.java b/src/main/java/com/vackosar/gitflowincrementalbuild/control/Property.java
index 45c8fee..a0dccc5 100644
--- a/src/main/java/com/vackosar/gitflowincrementalbuild/control/Property.java
+++ b/src/main/java/com/vackosar/gitflowincrementalbuild/control/Property.java
@@ -1,92 +1,134 @@
 package com.vackosar.gitflowincrementalbuild.control;
 
-import java.util.Optional;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
 import java.util.Properties;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public enum Property {
-    enabled("true"),
-
-
-    disableBranchComparison("false"),
-    referenceBranch("refs/remotes/origin/develop"),
-    fetchReferenceBranch("false"),
-    baseBranch("HEAD"),
-    fetchBaseBranch("false"),
-    compareToMergeBase("true"),
-    uncommited("true"),
-    untracked("true"),
-    excludePathRegex(Constants.NEVER_MATCH_REGEX),
-
-    buildAll("false"),
-    buildDownstream("always"),
-    buildUpstream("derived"),
-    buildUpstreamMode("changed"),
-    skipTestsForUpstreamModules("false", "skipTestsForNotImpactedModules"),
-    argsForUpstreamModules("", "argsForNotImpactedModules"),
-    forceBuildModules(""),
-    excludeTransitiveModulesPackagedAs(""),
-
-    failOnMissingGitDir("true"),
-    failOnError("true");
+    help("false", "h", true),
+    enabled("true", "e", true),
+
+    disableBranchComparison("false", "dbc", true),
+    referenceBranch("refs/remotes/origin/develop", "rb"),
+    fetchReferenceBranch("false", "frb", true),
+    baseBranch("HEAD", "bb"),
+    fetchBaseBranch("false", "fbb", true),
+    compareToMergeBase("true", "ctmb", true),
+    uncommited("true", "uc", true),
+    untracked("true", "ut", true),
+    excludePathRegex(Constants.NEVER_MATCH_REGEX, "epr"),
+
+    buildAll("false", "ba", true),
+    buildDownstream("always", "bd", true),
+    buildUpstream("derived", "bu", true),
+    buildUpstreamMode("changed", "bum"),
+    skipTestsForUpstreamModules("false", "stfum", true) {
+        @Override
+        public String deprecatedFullName() {
+            return PREFIX + "skipTestsForNotImpactedModules";
+        }
+    },
+    argsForUpstreamModules("", "afum") {
+        @Override
+        public String deprecatedFullName() {
+            return PREFIX + "argsForNotImpactedModules";
+        }
+    },
+    forceBuildModules("", "fbm"),
+    excludeTransitiveModulesPackagedAs("", "etmpa"),
+
+    failOnMissingGitDir("true", "fomgd", true),
+    failOnError("true", "foe", true);
 
     public static final String PREFIX = "gib.";
 
     private static final Logger LOGGER = LoggerFactory.getLogger(Property.class);
 
     private final String fullName;
+    private final String shortName;
     private final String defaultValue;
-    private final String deprecatedFullName;
+    private final List<String> allNames;
 
-    Property(String defaultValue) {
-        this(defaultValue, null);
+    private final boolean mapEmptyValueToTrue;
+
+    Property(String defaultValue, String unprefixedShortName) {
+        this(defaultValue, unprefixedShortName, false);
     }
 
-    Property(String defaultValue, String deprecatedFullName) {
+    Property(String defaultValue, String unprefixedShortName, boolean mapNoValueToTrue) {
         this.fullName = PREFIX + name();
         this.defaultValue = defaultValue;
-        this.deprecatedFullName = deprecatedFullName != null ? PREFIX + deprecatedFullName : null;
+        this.shortName = PREFIX + unprefixedShortName;
+        this.allNames = Stream.of(fullName, shortName, deprecatedFullName())
+                .filter(Objects::nonNull)
+                .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
+        this.mapEmptyValueToTrue = mapNoValueToTrue;
     }
 
     private String exemplify() {
-        return "<" + fullName() + ">" + ( defaultValue == null ? "" : defaultValue )+ "</" + fullName() + ">";
+        return String.format("%-85s", "<" + fullName + ">" + ( defaultValue == null ? "" : defaultValue ) + "</" + fullName + ">")
+                + " <!-- or <" + shortName + ">... -->";
     }
 
     public String fullName() {
         return fullName;
     }
 
+    public String shortName() {
+        return shortName;
+    }
+
+    // only for descriptive output
+    public String fullOrShortName() {
+        return fullName + " (or " + shortName + ")";
+    }
+
     public String deprecatedFullName() {
-        return deprecatedFullName;
+        // might be overridden by specific enum instances
+        return null;
+    }
+
+    public List<String> allNames() {
+        return allNames;
     }
 
     public String getValue(Properties projectProperties) {
-        String value = getValue(fullName, projectProperties);
-        if (value == null && deprecatedFullName != null) {
-            value = getValue(deprecatedFullName, projectProperties);
-            if (value != null) {
-                LOGGER.warn("{} has been replaced with {} and will be removed in an upcoming release. Please adjust your configuration!",
-                        deprecatedFullName, fullName);
-            }
-        }
-        if (value == null) {
-            value = defaultValue;
-        }
+        String value = Stream.of(System.getProperties(), projectProperties)
+                .flatMap(props -> allNames.stream()
+                        .map(name -> getValue(name, props)))
+                .filter(Objects::nonNull)
+                .findFirst()
+                .orElse(defaultValue);
+
         LOGGER.debug("{}={}", fullName, value);
         return value;
     }
 
-    private static String getValue(String fullName, Properties projectProperties) {
-        return Optional.ofNullable(System.getProperty(fullName)).orElseGet(() -> projectProperties.getProperty(fullName));
+    private String getValue(String name, Properties properties) {
+        String value = properties.getProperty(name);
+        if (value != null) {
+            if (name.equals(deprecatedFullName())) {
+                LOGGER.warn("{} has been replaced with {} and will be removed in an upcoming release. Please adjust your configuration!",
+                        deprecatedFullName(), fullOrShortName());
+            }
+            if (mapEmptyValueToTrue && value.isEmpty()) {
+                value = "true";
+            }
+        }
+        return value;
     }
 
     public static String exemplifyAll() {
         StringBuilder builder = new StringBuilder();
         builder.append("<properties>\n");
         for (Property value :Property.values()) {
-            builder.append("\t").append(value.exemplify()).append("\n");
+            builder.append("    ").append(value.exemplify()).append("\n");
         }
         builder.append("</properties>\n");
         return builder.toString();


Test patch:
diff --git a/src/test/java/com/vackosar/gitflowincrementalbuild/LoggerSpyUtil.java b/src/test/java/com/vackosar/gitflowincrementalbuild/LoggerSpyUtil.java
index 5e4a62b..d24fdfe 100644
--- a/src/test/java/com/vackosar/gitflowincrementalbuild/LoggerSpyUtil.java
+++ b/src/test/java/com/vackosar/gitflowincrementalbuild/LoggerSpyUtil.java
@@ -1,16 +1,17 @@
 package com.vackosar.gitflowincrementalbuild;
 
-import org.mockito.AdditionalAnswers;
 import org.mockito.Mockito;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.vackosar.gitflowincrementalbuild.mocks.MockitoWorkarounds;
+
 public class LoggerSpyUtil {
 
     public static Logger buildSpiedLoggerFor(Class<?> owningClazz) {
         Logger logger = LoggerFactory.getLogger(owningClazz);
         // note: cannot directly use Mockito.spy(logger) because logback loggers are final
-        Logger spiedLogger = Mockito.mock(Logger.class, AdditionalAnswers.delegatesTo(logger));
+        Logger spiedLogger = Mockito.mock(Logger.class, MockitoWorkarounds.AdditionalAnswers.delegatesTo(logger));
         return spiedLogger;
     }
 }
diff --git a/src/test/java/com/vackosar/gitflowincrementalbuild/SystemPropertiesResetRule.java b/src/test/java/com/vackosar/gitflowincrementalbuild/SystemPropertiesResetRule.java
new file mode 100644
index 0000000..1c7adee
--- /dev/null
+++ b/src/test/java/com/vackosar/gitflowincrementalbuild/SystemPropertiesResetRule.java
@@ -0,0 +1,29 @@
+package com.vackosar.gitflowincrementalbuild;
+
+import java.util.Properties;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Resets system properties to the state they had before the test method execution.<p/>
+ * Note: Could use https://github.com/stefanbirkner/system-rules instead, but for a simple rule this seems a bit much.
+ */
+public class SystemPropertiesResetRule implements TestRule {
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                final Properties backup = (Properties) System.getProperties().clone();
+                try {
+                    base.evaluate();
+                } finally {
+                    System.setProperties(backup);
+                }
+            }
+        };
+    }
+}
diff --git a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/ConfigurationTest.java b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/ConfigurationTest.java
index 3462d76..7cc9032 100644
--- a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/ConfigurationTest.java
+++ b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/ConfigurationTest.java
@@ -26,11 +26,11 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.RuleChain;
 import org.junit.runner.RunWith;
-import org.junit.runners.model.Statement;
 import org.mockito.Mock;
 import org.mockito.runners.MockitoJUnitRunner;
 
 import com.google.common.collect.ImmutableMap;
+import com.vackosar.gitflowincrementalbuild.SystemPropertiesResetRule;
 import com.vackosar.gitflowincrementalbuild.boundary.Configuration.BuildUpstreamMode;
 import com.vackosar.gitflowincrementalbuild.control.Property;
 
@@ -46,20 +46,7 @@ public class ConfigurationTest {
     private final ExpectedException thrown = ExpectedException.none();
 
     @Rule
-    public final RuleChain ruleChain = RuleChain
-            // properly reset system properties after each test
-            .outerRule((base, description) -> new Statement() {
-                @Override
-                public void evaluate() throws Throwable {
-                    final Properties backup = (Properties) System.getProperties().clone();
-                    try {
-                        base.evaluate();
-                    } finally {
-                        System.setProperties(backup);
-                    }
-                }
-            })
-            .around(thrown);
+    public final RuleChain ruleChain = RuleChain.outerRule(new SystemPropertiesResetRule()).around(thrown);
 
     @Mock
     private MavenExecutionRequest mavenExecutionRequestMock;
diff --git a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/MavenIntegrationTest.java b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/MavenIntegrationTest.java
index b52a925..11774ca 100644
--- a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/MavenIntegrationTest.java
+++ b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/MavenIntegrationTest.java
@@ -62,7 +62,7 @@ public class MavenIntegrationTest extends BaseRepoTest {
     /**
      * Installs all test artifacts/modules to avoid dependency problems when only building a subset incrementally.
      * <p/>
-     * This also downloads all required maven core and plugin dependencies into the test repo.
+     * This might download all required maven core and plugin dependencies into the test repo, but test setup in pom.xml tries to prevent that.
      * <p/>
      * This is performed only once for the entire class but cannot be moved to {@link BeforeClass} as {@link BaseRepoTest} (re-)creates the
      * test project for each test in {@link Before}.
@@ -105,6 +105,17 @@ public class MavenIntegrationTest extends BaseRepoTest {
     public void buildAllSkipTest() throws Exception {
         final String output = executeBuild(prop(Property.buildAll, "true"), prop(Property.skipTestsForUpstreamModules, "true"));
 
+        assertBuilAllSkipTest(output);
+    }
+
+    @Test
+    public void buildAllSkipTest_emptyPropertyValues() throws Exception {
+        final String output = executeBuild(prop(Property.buildAll, ""), prop(Property.skipTestsForUpstreamModules, ""));
+
+        assertBuilAllSkipTest(output);
+    }
+
+    private static void assertBuilAllSkipTest(final String output) {
         Assert.assertTrue(output.contains(" child1"));
         Assert.assertTrue(output.contains(" child2"));
         Assert.assertTrue(output.contains(" subchild1"));
@@ -116,7 +127,7 @@ public class MavenIntegrationTest extends BaseRepoTest {
         Assert.assertTrue(output.contains(" child6"));
         Assert.assertTrue(output.contains("[INFO] Tests are skipped."));
     }
-    
+
     @Test
     public void buildWithAlsoMake() throws Exception {
         final String output = executeBuild("-am");
@@ -222,6 +233,10 @@ public class MavenIntegrationTest extends BaseRepoTest {
     }
 
     private static String prop(Property property, String value) {
-        return "-D" + property.fullName() + "=" + value;
+        String propString =  "-D" + property.fullName();
+        if (value != null && !value.isEmpty()) {
+            propString += "=" + value;
+        }
+        return propString;
     }
 }
diff --git a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/MavenLifecycleParticipantTest.java b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/MavenLifecycleParticipantTest.java
index 8b61204..6e5c760 100644
--- a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/MavenLifecycleParticipantTest.java
+++ b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/MavenLifecycleParticipantTest.java
@@ -37,6 +37,8 @@ import static org.mockito.Mockito.when;
 @RunWith(MockitoJUnitRunner.class)
 public class MavenLifecycleParticipantTest {
 
+    private static final String TEST_IMPL_VERSION = "3.8.1";    // just an existing version, no need to be the latest one
+
     @Rule
     public final ExpectedException thrown = ExpectedException.none();
 
@@ -49,7 +51,7 @@ public class MavenLifecycleParticipantTest {
     private UnchangedProjectsRemover unchangedProjectsRemoverMock;
 
     @InjectMocks
-    private MavenLifecycleParticipant underTest;
+    private MavenLifecycleParticipant underTest = new MavenLifecycleParticipant(TEST_IMPL_VERSION);
 
     private final Properties projectProperties = new Properties();
 
@@ -66,6 +68,15 @@ public class MavenLifecycleParticipantTest {
         Whitebox.setInternalState(underTest, new Configuration.Provider(mavenSessionMock));
     }
 
+    @Test
+    public void defaultlyEnabled() throws Exception {
+
+        underTest.afterProjectsRead(mavenSessionMock);
+
+        verify(loggerSpy).info(contains("starting..."), eq(TEST_IMPL_VERSION));
+        verify(unchangedProjectsRemoverMock).act();
+    }
+
     @Test
     public void disabled() throws Exception {
         projectProperties.setProperty(Property.enabled.fullName(), "false");
@@ -78,14 +89,34 @@ public class MavenLifecycleParticipantTest {
     }
 
     @Test
-    public void defaultlyEnabled() throws Exception {
+    public void disabled_helpRequested() throws Exception {
+        projectProperties.setProperty(Property.enabled.fullName(), "false");
+        projectProperties.setProperty(Property.help.fullName(), "true");
+
+        underTest.afterProjectsRead(mavenSessionMock);
+
+        verifyHelpLogged(true);
+        verifyZeroInteractions(unchangedProjectsRemoverMock);
+    }
+
+    @Test
+    public void helpRequested() throws Exception {
+        projectProperties.setProperty(Property.help.fullName(), "true");
 
         underTest.afterProjectsRead(mavenSessionMock);
 
-        verify(loggerSpy).info(contains("starting..."));
+        verifyHelpLogged(true);
         verify(unchangedProjectsRemoverMock).act();
     }
 
+    @Test
+    public void defaultlyNoHelp() throws Exception {
+
+        underTest.afterProjectsRead(mavenSessionMock);
+
+        verifyHelpLogged(false);
+    }
+
     @Test
     public void onRuntimeException() throws Exception {
         RuntimeException runtimeException = new RuntimeException("FAIL !!!");
@@ -128,4 +159,9 @@ public class MavenLifecycleParticipantTest {
         verify(loggerSpy).warn(contains("ProjectDependencyGraph"));
         verifyZeroInteractions(unchangedProjectsRemoverMock);
     }
+
+    private void verifyHelpLogged(boolean logged) {
+        verify(loggerSpy, logged ? Mockito.times(1) : Mockito.never())
+                .info(contains("help:"), eq(TEST_IMPL_VERSION), anyString(), anyString(), anyString());
+    }
 }
diff --git a/src/test/java/com/vackosar/gitflowincrementalbuild/control/PropertyTest.java b/src/test/java/com/vackosar/gitflowincrementalbuild/control/PropertyTest.java
index 8726f97..3e261a8 100644
--- a/src/test/java/com/vackosar/gitflowincrementalbuild/control/PropertyTest.java
+++ b/src/test/java/com/vackosar/gitflowincrementalbuild/control/PropertyTest.java
@@ -1,15 +1,126 @@
 package com.vackosar.gitflowincrementalbuild.control;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.stream.Collectors;
+
+import org.junit.Rule;
 import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.vackosar.gitflowincrementalbuild.SystemPropertiesResetRule;
+
 public class PropertyTest {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(PropertyTest.class);
 
+    @Rule
+    public final SystemPropertiesResetRule sysPropResetRule = new SystemPropertiesResetRule();
+
     @Test
     public void exemplifyAll() {
         LOGGER.info("exemplifyAll():\n{}", Property.exemplifyAll());
     }
+
+    @Test
+    public void uniqueShortNames() {
+        Map<String, List<Property>> byShortName = Arrays.stream(Property.values())
+                .collect(Collectors.groupingBy(Property::shortName));
+        String nonUniquesString = byShortName.entrySet().stream()
+                .filter(e -> e.getValue().size() > 1)
+                .map(Object::toString)
+                .collect(Collectors.joining("\n\t"));
+        if (!nonUniquesString.isEmpty()) {
+            fail("Property.shortName clashed found:\n\t" + nonUniquesString);
+        }
+    }
+
+    @Test
+    public void getValue_unset() {
+        assertEquals("true", Property.enabled.getValue(new Properties()));
+    }
+
+    // fullName
+
+    @Test
+    public void getValue_fullName_properties() {
+        assertEquals("false", Property.enabled.getValue(propertiesWith(Property.enabled.fullName(), "false")));
+    }
+
+    @Test
+    public void getValue_fullName_systemProperties() {
+        System.setProperty(Property.enabled.fullName(), "false");
+
+        assertEquals("false", Property.enabled.getValue(new Properties()));
+    }
+
+    @Test
+    public void getValue_fullName_systemProperties_emptyValueMapsToTrue() {
+        // need to use a property that is false per default
+        System.setProperty(Property.buildAll.fullName(), "");
+
+        assertEquals("true", Property.buildAll.getValue(new Properties()));
+    }
+
+    @Test
+    public void getValue_fullName_systemProperties_emptyValueNotMapped() {
+        System.setProperty(Property.referenceBranch.fullName(), "");
+
+        assertEquals("", Property.referenceBranch.getValue(new Properties()));
+    }
+
+    @Test
+    public void getValue_fullName_systemProperties_override() {
+        System.setProperty(Property.enabled.fullName(), "true");
+
+        assertEquals("true", Property.enabled.getValue(propertiesWith(Property.enabled.fullName(), "false")));
+    }
+
+    @Test
+    public void getValue_fullName_systemProperties_override_shortName() {
+        System.setProperty(Property.enabled.shortName(), "true");
+
+        assertEquals("true", Property.enabled.getValue(propertiesWith(Property.enabled.fullName(), "false")));
+    }
+
+    // shortName
+
+    @Test
+    public void getValue_shortName_properties() {
+        assertEquals("false", Property.enabled.getValue(propertiesWith(Property.enabled.shortName(), "false")));
+    }
+
+    @Test
+    public void getValue_shortName_systemProperties() {
+        System.setProperty(Property.enabled.shortName(), "false");
+
+        assertEquals("false", Property.enabled.getValue(new Properties()));
+    }
+
+    @Test
+    public void getValue_shortName_systemProperties_override() {
+        System.setProperty(Property.enabled.shortName(), "true");
+
+        assertEquals("true", Property.enabled.getValue(propertiesWith(Property.enabled.shortName(), "false")));
+    }
+
+    @Test
+    public void getValue_shortName_systemProperties_override_fullNameWins() {
+        System.setProperty(Property.enabled.fullName(), "false");
+        System.setProperty(Property.enabled.shortName(), "true");
+
+        assertEquals("false", Property.enabled.getValue(new Properties()));
+    }
+
+    private static Properties propertiesWith(String propertyKey, String value) {
+        final Properties properties = new Properties();
+        properties.setProperty(propertyKey, value);
+        return properties;
+    }
 }
diff --git a/src/test/java/com/vackosar/gitflowincrementalbuild/mocks/MockitoWorkarounds.java b/src/test/java/com/vackosar/gitflowincrementalbuild/mocks/MockitoWorkarounds.java
new file mode 100644
index 0000000..65d5d88
--- /dev/null
+++ b/src/test/java/com/vackosar/gitflowincrementalbuild/mocks/MockitoWorkarounds.java
@@ -0,0 +1,96 @@
+package com.vackosar.gitflowincrementalbuild.mocks;
+
+import java.lang.reflect.Method;
+
+import org.mockito.internal.stubbing.defaultanswers.ForwardsInvocations;
+import org.mockito.invocation.Invocation;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/**
+ * Provides workarounds for bugs in Mockito.
+ */
+public class MockitoWorkarounds {
+
+    public static class AdditionalAnswers {
+
+        // workaround for https://github.com/mockito/mockito/issues/407 based on https://github.com/mockito/mockito/pull/412/files 
+        @SuppressWarnings("unchecked")
+        public static <T> Answer<T> delegatesTo(Object delegate) {
+            return (Answer<T>) new PatchedForwardsInvocations(delegate);
+        }
+
+        private static class PatchedForwardsInvocations extends ForwardsInvocations {
+
+            private static final long serialVersionUID = 1L;
+
+            public PatchedForwardsInvocations(Object delegatedObject) {
+                super(delegatedObject);
+            }
+
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                return super.answer(new InvocationOnMockWrapper(invocation) {
+
+                    private static final long serialVersionUID = 1L;
+
+                    @Override
+                    public Object[] getArguments() {
+                        return ((Invocation) invocation).getRawArguments();
+                    }
+                });
+            }
+        }
+    }
+
+    private static class InvocationOnMockWrapper implements InvocationOnMock {
+
+        private static final long serialVersionUID = 1L;
+
+        private final InvocationOnMock delegate;
+
+        public InvocationOnMockWrapper(InvocationOnMock delegate) {
+            this.delegate = delegate;
+        }
+
+        @Override
+        public Object getMock() {
+            return delegate.getMock();
+        }
+
+        @Override
+        public Method getMethod() {
+            return delegate.getMethod();
+        }
+
+        @Override
+        public Object[] getArguments() {
+            return delegate.getArguments();
+        }
+
+        @Override
+        public <T> T getArgumentAt(int index, Class<T> clazz) {
+            return delegate.getArgumentAt(index, clazz);
+        }
+
+        @Override
+        public Object callRealMethod() throws Throwable {
+            return delegate.callRealMethod();
+        }
+
+        @Override
+        public int hashCode() {
+            return delegate.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            return delegate.equals(obj);
+        }
+
+        @Override
+        public String toString() {
+            return delegate.toString();
+        }
+    }
+}