Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.github.danielflower.mavenplugins</groupId>
<artifactId>multi-module-maven-release-plugin</artifactId>
<version>3.7-SNAPSHOT</version> <!-- When changing also update scaffolding.TestProject.PLUGIN_VERSION_FOR_TESTS and add to src/site/markdown/changelog.md -->
<version>3.8-SNAPSHOT</version> <!-- When changing also update scaffolding.TestProject.PLUGIN_VERSION_FOR_TESTS and add to src/site/markdown/changelog.md -->

<name>The Multi Module Maven Release Plugin</name>
<description>A maven release plugin built for multi-maven-module git repositories allowing continuous deployment
Expand All @@ -27,6 +27,7 @@
<maven.project.version>2.2.1</maven.project.version>
<maven.model.version>3.9.9</maven.model.version>
<maven.settings.version>3.9.9</maven.settings.version>
<jgit.version>7.2.0.202503040940-r</jgit.version>
</properties>

<ciManagement>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;

import java.util.List;
import java.util.Set;

import static java.lang.String.format;

Expand Down Expand Up @@ -173,6 +174,42 @@ public abstract class BaseMojo extends AbstractMojo {
@Parameter(property = "arguments")
public String arguments;

/**
* <p>List of file system paths to ignore when detecting changes in the project(s).</p>
* <p>The primary purpose is to skip creating new releases if only "infrastructure" files such as
* <code>.gitignore</code>, <code>.editorconfig</code> and the like changed. Very basic wild cards are supported as
* follows:
* <ul>
* <li><code>/foo.txt</code> - matches <code>foo.txt</code> in the root of the top-level project</li>
* <li><code>/bar/foo.txt</code> - matches <code>foo.txt</code> in the root of the <code>bar</code> directory that resides in the root of the top-level project</li>
* <li><code>/bar/</code> - matches the <code>bar</code> directory and it's contents in the root of the top-level project</li>
* <li><code>foo.txt</code> - matches <code>foo.txt</code> anywhere in the project</li>
* <li><code>bar/foo.txt</code> - matches <code>foo.txt</code> in the <code>bar</code> directory anywhere in the project</li>
* <li><code>bar</code> - matches the <code>bar</code> directory anywhere in the project and ignores everything below</li>
* <ul/></p>
*/
@Parameter(property = "ignoredPaths")
Set<String> ignoredPaths;

/**
* <p>List of file system paths that are required to be changed when detecting changes in the project(s).</p>
* <p>If not set, any change triggers the release logic.</p>
* <p>The primary purpose is to trigger creating new releases only if certain changes occur in the repository.
* For example, a library repository with ProtoBuf messages would trigger release only if any of the *.proto files has been modified. Very basic wild cards are supported as
* follows:
* <ul>
* <li><code>/foo.txt</code> - matches <code>foo.txt</code> in the root of the top-level project</li>
* <li><code>/bar/foo.txt</code> - matches <code>foo.txt</code> in the root of the <code>bar</code> directory that resides in the root of the top-level project</li>
* <li><code>/bar/</code> - matches the <code>bar</code> directory and it's contents in the root of the top-level project</li>
* <li><code>foo.txt</code> - matches <code>foo.txt</code> anywhere in the project</li>
* <li><code>bar/foo.txt</code> - matches <code>foo.txt</code> in the <code>bar</code> directory anywhere in the project</li>
* <li><code>bar</code> - matches the <code>bar</code> directory anywhere in the project and ignores everything below</li>
* <ul/></p>
* <p><b>Excludes the <code>ignoredPaths</code> logic</b></p>
*/
@Parameter(property = "requiredPaths")
Set<String> requiredPaths;

final void setSettings(final Settings settings) {
this.settings = settings;
}
Expand Down Expand Up @@ -266,4 +303,35 @@ protected static String getRemoteUrlOrNullIfNoneSet(Scm originalScm, Scm actualS
return GitHelper.scmUrlToRemote(remote);
}

public MavenProject getProject() {
return project;
}

public List<MavenProject> getProjects() {
return projects;
}

public Long getBuildNumber() {
return buildNumber;
}

public String getTagNameFormat() {
return tagNameFormat;
}

public VersionNamer getVersionNamer() {
return versionNamer;
}

public List<String> getModulesToForceRelease() {
return modulesToForceRelease;
}

public Set<String> getIgnoredPaths() {
return ignoredPaths;
}

public Set<String> getRequiredPaths() {
return requiredPaths;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
.credentialsProvider(getCredentialsProvider(log))
.buildFromCurrentDir();
ResolverWrapper resolverWrapper = new ResolverWrapper(factory, artifactResolver, remoteRepositories, localRepository);
Reactor reactor = Reactor.fromProjects(log, repo, project, projects, buildNumber, modulesToForceRelease, noChangesAction, resolverWrapper, versionNamer, tagNameFormat);
Reactor reactor = Reactor.fromProjects(this, log, repo, noChangesAction, resolverWrapper);
if (reactor == null) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,18 @@ public List<ReleasableModule> getModulesInBuildOrder() {
return modulesInBuildOrder;
}

public static Reactor fromProjects(Log log, LocalGitRepo gitRepo, MavenProject rootProject, List<MavenProject> projects, Long buildNumber, List<String> modulesToForceRelease, NoChangesAction actionWhenNoChangesDetected, ResolverWrapper resolverWrapper, VersionNamer versionNamer, String tagNameFormat) throws ValidationException, GitAPIException, MojoExecutionException {
DiffDetector detector = new TreeWalkingDiffDetector(gitRepo.git.getRepository());
public static Reactor fromProjects(BaseMojo mojo, Log log, LocalGitRepo gitRepo, NoChangesAction actionWhenNoChangesDetected, ResolverWrapper resolverWrapper) throws ValidationException, GitAPIException, MojoExecutionException {
DiffDetector detector = new TreeWalkingDiffDetector(gitRepo.git.getRepository(), mojo.getIgnoredPaths(), mojo.getRequiredPaths());
List<ReleasableModule> modules = new ArrayList<ReleasableModule>();

resolveVersionsDefinedThroughProperties(projects);
resolveVersionsDefinedThroughProperties(mojo.getProjects());

AnnotatedTagFinder annotatedTagFinder = new AnnotatedTagFinder(versionNamer);
for (MavenProject project : projects) {
String relativePathToModule = calculateModulePath(rootProject, project);
AnnotatedTagFinder annotatedTagFinder = new AnnotatedTagFinder(mojo.getVersionNamer());
for (MavenProject project : mojo.getProjects()) {
String relativePathToModule = calculateModulePath(mojo.getProject(), project);
String artifactId = project.getArtifactId();
String versionWithoutBuildNumber = project.getVersion().replace("-SNAPSHOT", "");
List<AnnotatedTag> previousTagsForThisModule = annotatedTagFinder.tagsForVersion(gitRepo.git, project.getGroupId(), artifactId, versionWithoutBuildNumber, tagNameFormat, log);
List<AnnotatedTag> previousTagsForThisModule = annotatedTagFinder.tagsForVersion(gitRepo.git, project.getGroupId(), artifactId, versionWithoutBuildNumber, mojo.getTagNameFormat(), log);


Collection<Long> previousBuildNumbers = new ArrayList<Long>();
Expand All @@ -51,10 +51,10 @@ public static Reactor fromProjects(Log log, LocalGitRepo gitRepo, MavenProject r
}
}

Collection<Long> remoteBuildNumbers = getRemoteBuildNumbers(gitRepo, project.getGroupId(), artifactId, versionWithoutBuildNumber, versionNamer, tagNameFormat, log);
Collection<Long> remoteBuildNumbers = getRemoteBuildNumbers(gitRepo, project.getGroupId(), artifactId, versionWithoutBuildNumber, mojo.getVersionNamer(), mojo.getTagNameFormat(), log);
previousBuildNumbers.addAll(remoteBuildNumbers);

VersionName newVersion = versionNamer.name(project.getVersion(), buildNumber, previousBuildNumbers);
VersionName newVersion = mojo.getVersionNamer().name(project.getVersion(), mojo.getBuildNumber(), previousBuildNumbers);

boolean oneOfTheDependenciesHasChanged = false;
String changedDependency = null;
Expand Down Expand Up @@ -91,14 +91,14 @@ public static Reactor fromProjects(Log log, LocalGitRepo gitRepo, MavenProject r

String equivalentVersion = null;

if (modulesToForceRelease != null && modulesToForceRelease.contains(artifactId)) {
if (mojo.getModulesToForceRelease() != null && mojo.getModulesToForceRelease().contains(artifactId)) {
log.info("Releasing " + artifactId + " " + newVersion.releaseVersion() + " as we were asked to force release.");
} else if (oneOfTheDependenciesHasChanged) {
log.info("Releasing " + artifactId + " " + newVersion.releaseVersion() + " as " + changedDependency + " has changed.");
} else {
AnnotatedTag previousTagThatIsTheSameAsHEADForThisModule = hasChangedSinceLastRelease(previousTagsForThisModule, detector, project, relativePathToModule);
if (previousTagThatIsTheSameAsHEADForThisModule != null) {
equivalentVersion = previousTagThatIsTheSameAsHEADForThisModule.version() + versionNamer.getDelimiter() + previousTagThatIsTheSameAsHEADForThisModule.buildNumber();
equivalentVersion = previousTagThatIsTheSameAsHEADForThisModule.version() + mojo.getVersionNamer().getDelimiter() + previousTagThatIsTheSameAsHEADForThisModule.buildNumber();
// attempt to resolve
if (resolverWrapper.isResolvable(project.getGroupId(), project.getArtifactId(), equivalentVersion, project.getPackaging(), log)) {
log.info("Will use version " + equivalentVersion + " for " + artifactId + " as it has not been changed since that release.");
Expand All @@ -110,7 +110,7 @@ public static Reactor fromProjects(Log log, LocalGitRepo gitRepo, MavenProject r
log.info("Will use version " + newVersion.releaseVersion() + " for " + artifactId + " as it has changed since the last release.");
}
}
ReleasableModule module = new ReleasableModule(project, newVersion, equivalentVersion, relativePathToModule, tagNameFormat, log);
ReleasableModule module = new ReleasableModule(project, newVersion, equivalentVersion, relativePathToModule, mojo.getTagNameFormat(), log);
modules.add(module);
}

Expand Down Expand Up @@ -159,7 +159,7 @@ private static Collection<Long> getRemoteBuildNumbers(LocalGitRepo gitRepo, Stri
Collection<Ref> remoteTagRefs = gitRepo.allTags();
Collection<Long> remoteBuildNumbers = new ArrayList<Long>();
String tagWithoutBuildNumber = artifactId + "-" + versionWithoutBuildNumber;
if(StringUtils.isNotEmpty(tagNameFormat)) {
if (StringUtils.isNotEmpty(tagNameFormat)) {
tagWithoutBuildNumber = AnnotatedTag.formatTagName(tagWithoutBuildNumber, groupId, artifactId, versionWithoutBuildNumber, tagNameFormat, log);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
repo.errorIfNotClean(ignoredUntrackedPaths);

ResolverWrapper resolverWrapper = new ResolverWrapper(factory, artifactResolver, remoteRepositories, localRepository);
Reactor reactor = Reactor.fromProjects(log, repo, project, projects, buildNumber, modulesToForceRelease, noChangesAction, resolverWrapper, versionNamer, tagNameFormat);
Reactor reactor = Reactor.fromProjects(this, log, repo, noChangesAction, resolverWrapper);
if (reactor == null) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,44 @@
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
import org.eclipse.jgit.treewalk.filter.OrTreeFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.PathSuffixFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class TreeWalkingDiffDetector implements DiffDetector {

private final Repository repo;
private final Set<String> ignoredPaths;
private final Set<String> requiredPaths;

public TreeWalkingDiffDetector(Repository repo) {
public TreeWalkingDiffDetector(Repository repo, Set<String> ignoredPaths, Set<String> requiredPaths) {
this.repo = repo;
this.ignoredPaths = ignoredPaths != null ? ignoredPaths : new HashSet<>();
this.requiredPaths = requiredPaths;
}

public boolean hasChangedSince(String modulePath, java.util.List<String> childModules, Collection<AnnotatedTag> tags) throws IOException {
RevWalk walk = new RevWalk(repo);
try {
walk.setRetainBody(false);
walk.markStart(walk.parseCommit(repo.getRefDatabase().findRef("HEAD").getObjectId()));
filterOutOtherModulesChanges(modulePath, childModules, walk);
List<TreeFilter> treeFilters = new ArrayList<>();
filterOutOtherModulesChanges(modulePath, childModules, treeFilters);
if (requiredPaths == null || requiredPaths.isEmpty()) {
filterOutIgnoredPathsChanges(treeFilters);
} else {
filterRequiredPathsChanges(treeFilters);
}
walk.setTreeFilter(treeFilters.size() == 1 ? treeFilters.get(0) : AndTreeFilter.create(treeFilters));
stopWalkingWhenTheTagsAreHit(tags, walk);
return walk.iterator().hasNext();
} finally {
Expand All @@ -42,10 +58,9 @@ private static void stopWalkingWhenTheTagsAreHit(Collection<AnnotatedTag> tags,
}
}

private void filterOutOtherModulesChanges(String modulePath, List<String> childModules, RevWalk walk) {
private void filterOutOtherModulesChanges(String modulePath, List<String> childModules, List<TreeFilter> treeFilters) {
boolean isRootModule = ".".equals(modulePath);
boolean isMultiModuleProject = !isRootModule || !childModules.isEmpty();
List<TreeFilter> treeFilters = new ArrayList<>();
treeFilters.add(TreeFilter.ANY_DIFF);
if (isMultiModuleProject) {
if (!isRootModule) {
Expand All @@ -60,7 +75,34 @@ private void filterOutOtherModulesChanges(String modulePath, List<String> childM
}

}
TreeFilter treeFilter = treeFilters.size() == 1 ? treeFilters.get(0) : AndTreeFilter.create(treeFilters);
walk.setTreeFilter(treeFilter);
}

private void filterOutIgnoredPathsChanges(List<TreeFilter> treeFilters) {
for (String ignoredPath : ignoredPaths) {
TreeFilter filter;
if (ignoredPath.startsWith("/")) {
filter = PathFilter.create(ignoredPath.substring(1));
} else {
filter = PathSuffixFilter.create(ignoredPath);
}
treeFilters.add(filter.negate());
}
}

private void filterRequiredPathsChanges(List<TreeFilter> treeFilters) {
List<TreeFilter> filters = requiredPaths.stream()
.filter(requiredPath -> !requiredPath.isEmpty())
.map(requiredPath -> {
if (requiredPath.startsWith("/")) {
return PathFilter.create(requiredPath.substring(1));
} else {
return PathSuffixFilter.create(requiredPath);
}
})
.collect(Collectors.toList());
if (filters.isEmpty()) {
return;
}
treeFilters.add(filters.size() == 1 ? filters.get(0) : OrTreeFilter.create(filters));
}
}
4 changes: 4 additions & 0 deletions src/site/markdown/changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
Changelog
---------

### 3.8.0

* Added `ignorePaths` and `requiredPaths` settings

### 3.7.8

* Fixed null pointer exception when getting location of certain dependencies.
Expand Down
45 changes: 44 additions & 1 deletion src/site/markdown/usage.md.vm
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,47 @@ The supported report formats are FLAT(line separated) and JSON.
<releasedModulesOnly>false</releasedModulesOnly>
</versionReport>
</versionReports>
```
```

Excluding certain files changes from triggering the release
-----------------------------------------------------------

Certain file changes could be excluded from the release triggering process.

mvn releaser:release -D"ignorePaths=file1.txt,/file2.txt"

Or through the plugin configuration section:

```xml
<configuration>
<ignorePaths>
<ignoredPath>file1.txt</ignoredPath>
<ignoredPath>/file2.txt</ignoredPath>
</ignorePaths>
</configuration>
```

Include only certain files changes for release triggering
-----------------------------------------------------------

The primary purpose is to trigger creating new releases only if certain changes occur in the repository.
If not set, any change triggers the release logic.
For example, a library repository with ProtoBuf messages would trigger release only if any of the *.proto files has been modified.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you specify what wildcards are or aren't allowed - e.g. is it regex, glob patterns, or something else. And in the example exclude *.proto so it's obvious how to do that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added clarification on how the plugin works if the option is not set and an explanation of the path matching logic. If something could be improved further, please give me a notice.

This setting disables `ignorePaths`.

Path matching supports only absolute paths (which start with a `/`) and suffix path matching.
For example, `/file.txt` will match only file.txt that resides in the repository's root. `file.txt` will match all files whose path ends with file.txt - `/file.txt`, `/module/file.txt`, etc.
No wildcards or regexp allowed.

mvn releaser:release -D"requiredPaths=file1.txt,/file2.txt"

Or through the plugin configuration section:

```xml
<configuration>
<reauiredPaths>
<requiredPath>file1.txt</requiredPath>
<requiredPath>/file2.txt</requiredPath>
</reauiredPaths>
</configuration>
```
Loading