Repository: vackosar/gitflow-incremental-builder (java)

Issue:
Write impacted modules to a configurable "logfile"
I recently stumbled upon a case where I had to run a separate Maven process per certain module (to test `quarkus:dev` for multiple modules in a "modular monolith" project).

To run this only for modified projects, it would come in handy if `GIB` could write the list of "impacted" modules to a file that can then be consumed by subsequent processes.



Pull request description:
Issue #164: introduce logImpactedTo
Resolves #164 

Patch:
diff --git a/README.md b/README.md
index 7bb6229..11f8d69 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,7 @@ This extension is **not limited to Git Flow setups!** The [extensive configurati
   - [gib.argsForUpstreamModules](#gibargsforupstreammodules)
   - [gib.forceBuildModules](#gibforcebuildmodules)
   - [gib.excludeDownstreamModulesPackagedAs](#gibexcludedownstreammodulespackagedas)
+  - [gib.logImpactedTo](#giblogimpactedto)
 
 - [Explicitly selected projects](#explicitly-selected-projects)
   - [mvn -pl](#mvn--pl)
@@ -288,6 +289,7 @@ Maven pom properties configuration with default values is below:
     <gib.excludeDownstreamModulesPackagedAs></gib.excludeDownstreamModulesPackagedAs>     <!-- or <gib.edmpa>... -->
     <gib.failOnMissingGitDir>true</gib.failOnMissingGitDir>                               <!-- or <gib.fomgd>... -->
     <gib.failOnError>true</gib.failOnError>                                               <!-- or <gib.foe>... -->
+    <gib.logImpactedTo></gib.logImpactedTo>                                               <!-- or <gib.lit>... -->
 </properties>
 ```
 
@@ -487,6 +489,16 @@ deployment modules will be built.
 
 This property has no effect in case `buildAll` is enabled and an exclusion might be overriden by `gib.forceBuildModules`.
 
+### gib.logImpactedTo
+
+Defines an optional logfile which GIB shall write all "impacted" modules to. Each line represents the base directory of a changed module
+or a downstream module of a changed module.
+
+GIB overwrites the file if it already exists and will create an empty file in case no changes are detected
+or only [explicitly selected projects](#explicitly-selected-projects) are present.
+
+Since: 3.10.1
+
 ## Explicitly selected projects
 
 ### mvn -pl
diff --git a/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/Configuration.java b/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/Configuration.java
index 721b11d..64c93b5 100644
--- a/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/Configuration.java
+++ b/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/Configuration.java
@@ -8,6 +8,8 @@ import org.apache.maven.execution.MavenSession;
 import static java.util.stream.Collectors.collectingAndThen;
 import static java.util.stream.Collectors.toList;
 
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.AbstractMap;
 import java.util.Arrays;
 import java.util.Collections;
@@ -54,6 +56,7 @@ public class Configuration {
 
     public final boolean failOnMissingGitDir;
     public final boolean failOnError;
+    public final Optional<Path> logImpactedTo;
 
     private Configuration(MavenSession session) {
         Properties projectProperties = getProjectProperties(session);
@@ -96,6 +99,7 @@ public class Configuration {
 
         failOnMissingGitDir = Boolean.valueOf(Property.failOnMissingGitDir.getValue(projectProperties));
         failOnError = Boolean.valueOf(Property.failOnError.getValue(projectProperties));
+        logImpactedTo = Optional.ofNullable(Property.logImpactedTo.getValue(projectProperties)).map(Paths::get);
     }
 
     /**
diff --git a/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemover.java b/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemover.java
index 95b5ac6..96ff161 100644
--- a/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemover.java
+++ b/src/main/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemover.java
@@ -16,6 +16,10 @@ import javax.inject.Singleton;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
 import java.util.*;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -37,6 +41,9 @@ class UnchangedProjectsRemover {
     @Inject private Configuration.Provider configProvider;
 
     void act() throws GitAPIException, IOException {
+        // ensure to write logfile for impaced (even if just empty)
+        configProvider.get().logImpactedTo.ifPresent(logFilePath -> writeImpactedLogFile(Collections.emptySet(), logFilePath));
+
         // before checking for any changes, check whether there are _only_ explicitly selected projects (-pl) which have the highest priority
         final Set<MavenProject> selected = ProjectSelectionUtil.gatherSelectedProjects(mavenSession);
         if (onlySelectedModulesPresent(selected)) {
@@ -63,6 +70,8 @@ class UnchangedProjectsRemover {
 
         final Set<MavenProject> impacted = calculateImpactedProjects(selected, changed);
 
+        configProvider.get().logImpactedTo.ifPresent(logFilePath -> writeImpactedLogFile(impacted, logFilePath));
+
         if (!configProvider.get().buildAll) {
             modifyProjectList(selected, changed, impacted);
         } else {
@@ -72,6 +81,21 @@ class UnchangedProjectsRemover {
         }
     }
 
+    private void writeImpactedLogFile(Set<MavenProject> impacted, Path logFilePath) {
+        List<String> projectsToLog = impacted.isEmpty()
+                ? Collections.emptyList()
+                : mavenSession.getProjects().stream()   // write in proper order
+                        .filter(impacted::contains)
+                        .map(proj -> proj.getBasedir().getPath())
+                        .collect(Collectors.toList());
+        logger.debug("Writing impacted projects to {}: {}", logFilePath, projectsToLog);
+        try {
+            Files.write(logFilePath, projectsToLog, StandardCharsets.UTF_8, StandardOpenOption.CREATE);
+        } catch (IOException e) {
+            throw new IllegalStateException("Failed to write impacted projects to " + logFilePath + ": " + impacted, e);
+        }
+    }
+
     private boolean onlySelectedModulesPresent(Set<MavenProject> selected) {
         return !selected.isEmpty() && mavenSession.getProjects().equals(new ArrayList<>(selected));
     }
@@ -115,7 +139,9 @@ class UnchangedProjectsRemover {
         if (cfg.buildAll || cfg.buildDownstream) {
             impacted = impacted.flatMap(this::streamProjectWithDownstreamProjects);
         }
-        return impacted.collect(Collectors.toCollection(LinkedHashSet::new));
+        return impacted
+                .filter(mavenSession.getProjects()::contains)   // not deselected
+                .collect(Collectors.toCollection(LinkedHashSet::new));
     }
 
     private void modifyProjectList(Set<MavenProject> selected, Set<MavenProject> changed, Set<MavenProject> impacted) {
@@ -148,9 +174,8 @@ class UnchangedProjectsRemover {
                     .collect(Collectors.toCollection(LinkedHashSet::new));
             switch (buildUpstreamMode) {
                 case NONE:
-                    // use impacted (without deselected)
+                    // just use impacted that are selected and the downstreams of the selected
                     return impacted.stream()
-                            .filter(mavenSession.getProjects()::contains)   // not deselected
                             .filter(selectedWithDownstream::contains)
                             .collect(Collectors.toCollection(LinkedHashSet::new));
                 case CHANGED:
@@ -169,10 +194,8 @@ class UnchangedProjectsRemover {
             Set<MavenProject> upstreamRequiringProjects;
             switch (buildUpstreamMode) {
                 case NONE:
-                    // just use impacted (without deselected)
-                    return impacted.stream()
-                            .filter(mavenSession.getProjects()::contains)   // not deselected
-                            .collect(Collectors.toCollection(LinkedHashSet::new));
+                    // just use impacted
+                    return impacted;
                 case CHANGED:
                     upstreamRequiringProjects = changed;
                     break;
diff --git a/src/main/java/com/vackosar/gitflowincrementalbuild/control/Property.java b/src/main/java/com/vackosar/gitflowincrementalbuild/control/Property.java
index f8a1868..2ef6a1b 100644
--- a/src/main/java/com/vackosar/gitflowincrementalbuild/control/Property.java
+++ b/src/main/java/com/vackosar/gitflowincrementalbuild/control/Property.java
@@ -34,7 +34,7 @@ public enum Property {
     skipTestsForUpstreamModules("false", "stfum", true),
     argsForUpstreamModules("", "afum"),
     forceBuildModules("", "fbm"),
-    excludeDownstreamModulesPackagedAs("", "edmpa"){
+    excludeDownstreamModulesPackagedAs("", "edmpa") {
         @Override
         public String deprecatedFullName() {
             return PREFIX + "excludeTransitiveModulesPackagedAs";
@@ -42,7 +42,8 @@ public enum Property {
     },
 
     failOnMissingGitDir("true", "fomgd", true),
-    failOnError("true", "foe", true);
+    failOnError("true", "foe", true),
+    logImpactedTo(null, "lit");
 
     public static final String PREFIX = "gib.";
 


Test patch:
diff --git a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/BaseUnchangedProjectsRemoverTest.java b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/BaseUnchangedProjectsRemoverTest.java
index c297129..f4fb45b 100644
--- a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/BaseUnchangedProjectsRemoverTest.java
+++ b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/BaseUnchangedProjectsRemoverTest.java
@@ -5,6 +5,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.withSettings;
 
+import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -34,7 +35,7 @@ import com.vackosar.gitflowincrementalbuild.control.Property;
 
 /**
  * Tests {@link UnchangedProjectsRemover} with Mockito mocks.
- * 
+ *
  * @author famod
  */
 @RunWith(MockitoJUnitRunner.class)
@@ -57,7 +58,7 @@ public abstract class BaseUnchangedProjectsRemoverTest {
     @Mock(lenient = true)
     protected MavenSession mavenSessionMock;
 
-    @Mock
+    @Mock(lenient = true)
     protected ProjectDependencyGraph projectDependencyGraphMock;
 
     @Mock
@@ -105,6 +106,7 @@ public abstract class BaseUnchangedProjectsRemoverTest {
         MavenProject newModuleMock = mock(MavenProject.class, withSettings().name(moduleArtifactId).lenient());
         allModuleMocks.add(newModuleMock);
         when(newModuleMock.getArtifactId()).thenReturn(moduleArtifactId);
+        when(newModuleMock.getBasedir()).thenReturn(new File(moduleArtifactId));
         when(newModuleMock.getPackaging()).thenReturn(packaging);
         if (addToChanged) {
             changedProjects.add(newModuleMock);
diff --git a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemoverLogImpactedTest.java b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemoverLogImpactedTest.java
new file mode 100644
index 0000000..9ba695b
--- /dev/null
+++ b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemoverLogImpactedTest.java
@@ -0,0 +1,91 @@
+package com.vackosar.gitflowincrementalbuild.boundary;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+import org.apache.maven.project.MavenProject;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import com.vackosar.gitflowincrementalbuild.control.Property;
+
+/**
+ * Tests {@link UnchangedProjectsRemover} with Mockito mocks in context of {@link Property#logImpactedTo}.
+ *
+ * @author famod
+ */
+public class UnchangedProjectsRemoverLogImpactedTest extends BaseUnchangedProjectsRemoverTest {
+
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    private Path logFilePath;
+
+    @Override
+    @Before
+    public void before() throws GitAPIException, IOException {
+        super.before();
+
+        logFilePath = temporaryFolder.getRoot().toPath().resolve("impacted.log");
+        addGibProperty(Property.logImpactedTo, logFilePath.toAbsolutePath().toString());
+    }
+
+    @Test
+    public void logImpatcedTo_nothingChanged() throws GitAPIException, IOException {
+        addModuleMock(AID_MODULE_B, false);
+
+        underTest.act();
+
+        assertLogFileContains();
+    }
+
+    @Test
+    public void singleChanged() throws GitAPIException, IOException {
+        MavenProject changedModuleMock = addModuleMock(AID_MODULE_B, true);
+
+        underTest.act();
+
+        assertLogFileContains(changedModuleMock);
+    }
+
+    @Test
+    public void singleChanged_withDownstream() throws GitAPIException, IOException {
+        MavenProject changedModuleMock = addModuleMock(AID_MODULE_B, true);
+        MavenProject dependentModuleMock = addModuleMock(AID_MODULE_C, false);
+        MavenProject independentModuleMock = addModuleMock(AID_MODULE_D, false);
+
+        setUpstreamProjects(dependentModuleMock, changedModuleMock, moduleA);
+        setDownstreamProjects(changedModuleMock, dependentModuleMock);
+        setUpstreamProjects(independentModuleMock, moduleA);
+
+        underTest.act();
+
+        assertLogFileContains(changedModuleMock, dependentModuleMock);
+    }
+
+    @Test
+    public void singleChanged_buildUpstream() throws GitAPIException, IOException {
+        MavenProject changedModuleMock = addModuleMock(AID_MODULE_B, true);
+
+        addGibProperty(Property.buildUpstream, "true");
+
+        underTest.act();
+
+        assertLogFileContains(changedModuleMock);
+    }
+
+    private void assertLogFileContains(MavenProject... mavenProjects) throws IOException {
+        assertTrue(logFilePath + " is missing", Files.isReadable(logFilePath));
+        assertEquals("Unexpected content of " + logFilePath,
+                Arrays.stream(mavenProjects).map(proj -> proj.getBasedir().getPath()).collect(Collectors.toList()),
+                Files.readAllLines(logFilePath));
+    }
+}
diff --git a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemoverSelectedProjectsTest.java b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemoverSelectedProjectsTest.java
index 9e6b16e..014a6e0 100644
--- a/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemoverSelectedProjectsTest.java
+++ b/src/test/java/com/vackosar/gitflowincrementalbuild/boundary/UnchangedProjectsRemoverSelectedProjectsTest.java
@@ -417,7 +417,6 @@ public class UnchangedProjectsRemoverSelectedProjectsTest extends BaseUnchangedP
 
         addGibProperty(Property.buildDownstream, "false");
         addGibProperty(Property.buildAllIfNoChanges, "true");
-        addGibProperty(Property.skipTestsForUpstreamModules, "true");
 
         changedProjects.add(moduleA);
 
@@ -540,7 +539,6 @@ public class UnchangedProjectsRemoverSelectedProjectsTest extends BaseUnchangedP
         overrideProjects(moduleA, moduleC);
 
         addGibProperty(Property.buildDownstream, "false");
-        addGibProperty(Property.skipTestsForUpstreamModules, "true");
 
         underTest.act();
 
@@ -563,7 +561,6 @@ public class UnchangedProjectsRemoverSelectedProjectsTest extends BaseUnchangedP
 
         addGibProperty(Property.buildDownstream, "false");
         addGibProperty(Property.buildAllIfNoChanges, "true");
-        addGibProperty(Property.skipTestsForUpstreamModules, "true");
 
         underTest.act();
 
@@ -586,7 +583,6 @@ public class UnchangedProjectsRemoverSelectedProjectsTest extends BaseUnchangedP
         overrideProjects(moduleA, moduleC);
 
         addGibProperty(Property.buildUpstream, "true");
-        addGibProperty(Property.skipTestsForUpstreamModules, "true");
 
         underTest.act();
 
@@ -609,7 +605,6 @@ public class UnchangedProjectsRemoverSelectedProjectsTest extends BaseUnchangedP
         overrideProjects(moduleA, moduleC);
 
         addGibProperty(Property.buildUpstream, "true");
-        addGibProperty(Property.skipTestsForUpstreamModules, "true");
         addGibProperty(Property.buildDownstream, "false");
 
         underTest.act();