findUncommittedChanges(Repository repository) throws IOException {
+ try (Git git = new Git(repository)) {
+ Status status = git.status().call();
+
+ return Streams.concat(status.getUncommittedChanges().stream(), status.getUntracked().stream())
+ .map(ModifiedFilePath::new);
+ } catch (GitAPIException e) {
+ throw new IOException("Unable to find uncommitted changes", e);
+ }
+ }
+
+}
diff --git a/spring-modulith-junit/src/main/java/org/springframework/modulith/junit/diff/UnpushedCommitsDetector.java b/spring-modulith-junit/src/main/java/org/springframework/modulith/junit/diff/UnpushedCommitsDetector.java
new file mode 100644
index 00000000..0995e9e5
--- /dev/null
+++ b/spring-modulith-junit/src/main/java/org/springframework/modulith/junit/diff/UnpushedCommitsDetector.java
@@ -0,0 +1,41 @@
+package org.springframework.modulith.junit.diff;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.lib.BranchConfig;
+import org.springframework.core.env.PropertyResolver;
+import org.springframework.lang.NonNull;
+
+/**
+ *
+ * Find all changes that have not been pushed to the remote branch yet.
+ *
+ * To be precise, this finds the diff between the local HEAD and its tracking branch and the uncommitted and untracked
+ * changes. Note: This will not fetch from the remote first!
+ */
+public class UnpushedCommitsDetector implements FileModificationDetector {
+
+ @Override
+ public @NonNull Set getModifiedFiles(@NonNull PropertyResolver propertyResolver)
+ throws IOException {
+ try (var repo = JGitUtil.buildRepository()) {
+ String localBranch = repo.getFullBranch();
+ String trackingBranch = new BranchConfig(repo.getConfig(), repo.getBranch()).getTrackingBranch();
+
+ Stream diff = localBranch != null && trackingBranch != null
+ ? JGitUtil.diffRefs(repo, localBranch, trackingBranch)
+ : Stream.empty();
+
+ HashSet result = new HashSet<>();
+ result.addAll(new UncommittedChangesDetector().getModifiedFiles(propertyResolver));
+ result.addAll(JGitUtil.convertDiffEntriesToFileChanges(diff).collect(Collectors.toSet()));
+ return result;
+ }
+ }
+
+}
diff --git a/spring-modulith-junit/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-modulith-junit/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 00000000..d96288d6
--- /dev/null
+++ b/spring-modulith-junit/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,17 @@
+{
+ "groups": [
+ {
+ "name": "spring.modulith.test",
+ "description": "Properties configuring the execution of modulith-specific tests."
+ }
+ ],
+ "properties": [
+ {
+ "name": "spring.modulith.test.changed-files-strategy",
+ "type": "java.lang.String",
+ "defaultValue": "org.springframework.modulith.UncommittedChangesStrategy",
+ "description": "A strategy to determine the list of changed files to consider for test execution. This should be a fully qualified class name of a registered implementation."
+ }
+ ],
+ "hints": []
+}
diff --git a/spring-modulith-junit/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/spring-modulith-junit/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
new file mode 100644
index 00000000..924a6a1b
--- /dev/null
+++ b/spring-modulith-junit/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
@@ -0,0 +1 @@
+org.springframework.modulith.junit.ModulithExecutionExtension
diff --git a/spring-modulith-junit/src/main/resources/META-INF/services/org.springframework.modulith.FileModificationDetector b/spring-modulith-junit/src/main/resources/META-INF/services/org.springframework.modulith.FileModificationDetector
new file mode 100644
index 00000000..b96b4406
--- /dev/null
+++ b/spring-modulith-junit/src/main/resources/META-INF/services/org.springframework.modulith.FileModificationDetector
@@ -0,0 +1,3 @@
+org.springframework.modulith.git.UncommittedChangesDetector
+org.springframework.modulith.git.UnpushedGitChangesDetector
+org.springframework.modulith.git.DiffDetector
diff --git a/spring-modulith-junit/src/main/resources/junit-platform.properties b/spring-modulith-junit/src/main/resources/junit-platform.properties
new file mode 100644
index 00000000..6efc0d5e
--- /dev/null
+++ b/spring-modulith-junit/src/main/resources/junit-platform.properties
@@ -0,0 +1 @@
+junit.jupiter.extensions.autodetection.enabled=true
diff --git a/spring-modulith-junit/src/test/java/org/springframework/modulith/junit/ChangeTest.java b/spring-modulith-junit/src/test/java/org/springframework/modulith/junit/ChangeTest.java
new file mode 100644
index 00000000..0531b634
--- /dev/null
+++ b/spring-modulith-junit/src/test/java/org/springframework/modulith/junit/ChangeTest.java
@@ -0,0 +1,33 @@
+package org.springframework.modulith.junit;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.modulith.junit.Change.JavaClassChange;
+import org.springframework.modulith.junit.Change.JavaTestClassChange;
+import org.springframework.modulith.junit.Change.OtherFileChange;
+import org.springframework.modulith.junit.diff.ModifiedFilePath;
+
+class ChangesTest {
+ @Test
+ void shouldInterpredModifiedFilePathsCorrectly() {
+ // given
+ Set modifiedFilePaths = Set.of(
+ new ModifiedFilePath("spring-modulith-junit/src/main/java/org/springframework/modulith/Changes.java"),
+ new ModifiedFilePath("spring-modulith-junit/src/test/java/org/springframework/modulith/ChangesTest.java"),
+ new ModifiedFilePath(
+ "spring-modulith-junit/src/main/resources/META-INF/additional-spring-configuration-metadata.json"));
+
+ // when
+ Set result = Changes.toChanges(modifiedFilePaths);
+
+ // then
+ assertThat(result).containsExactlyInAnyOrder(
+ new JavaClassChange("org.springframework.modulith.Changes"),
+ new JavaTestClassChange("org.springframework.modulith.ChangesTest"),
+ new OtherFileChange(
+ "spring-modulith-junit/src/main/resources/META-INF/additional-spring-configuration-metadata.json"));
+ }
+}