Skip to content

Commit

Permalink
Add ability to reject versions by regular expression (#679)
Browse files Browse the repository at this point in the history
For each module, it is now possible to exclude some versions from
upgrades, for example:

```gradle
tasks.named("updateVersionCatalogs") {
    rejectedVersionsPerModule['awesome.lib:awesome'] = '3[.]0.[8-9]'
}
```
  • Loading branch information
melix committed Apr 15, 2024
1 parent 9eb2e59 commit 30f106f
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 72 deletions.
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ tasks.register('functionalTest', Test) {
systemProperty "CURRENT_JDK", JavaVersion.current().majorVersion
environment("GH_USERNAME", System.getenv("GH_USERNAME"))
environment("GH_TOKEN_PUBLIC_REPOS_READONLY", System.getenv("GH_TOKEN_PUBLIC_REPOS_READONLY"))
environment("CI", System.getenv("CI"))
if (System.getenv("CI") != null) {
environment("CI", System.getenv("CI"))
}
}

tasks.named('check') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ class VersionCatalogUpdateFunctionalTest extends AbstractFunctionalTest {
repository.when(
request()
).respond(new LoggingCallback())
if (idx == 7) {
buildFile << """
tasks.named("updateVersionCatalogs") {
rejectedVersionsPerModule['awesome.lib:awesome'] = '3\\\\.0\\\\.[8-9]'
}
"""
}
run 'useLatestVersions'

then:
Expand All @@ -88,7 +95,7 @@ class VersionCatalogUpdateFunctionalTest extends AbstractFunctionalTest {
catalogFile.text == expected

where:
idx << (0..6)
idx << (0..7)
}

static class LoggingCallback implements ExpectationResponseCallback {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[versions]
awesome = "3.0.6"

[libraries]
awesome = { module = "awesome.lib:awesome", version.ref = "awesome" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[versions]
awesome = "3.0.7"

[libraries]
awesome = { module = "awesome.lib:awesome", version.ref = "awesome" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>awesome.lib</groupId>
<artifactId>awesome</artifactId>
<version>3.0.7</version>
<packaging>jar</packaging>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public void apply(Project project) {
task.getOutputDirectory().convention(project.getLayout().getBuildDirectory().dir("catalogs-update"));
task.getRejectedQualifiers().convention(Arrays.asList("alpha", "beta", "rc", "cr", "m", "preview", "b", "ea"));
task.getIgnoredModules().convention(Collections.emptySet());
task.getRejectedVersionsPerModule().convention(Collections.emptyMap());
task.getAllowMajorUpdates().convention(false);
});
tasks.register("useLatestVersions", Copy.class, task -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.gradle.api.artifacts.result.UnresolvedDependencyResult;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.SetProperty;
import org.gradle.api.tasks.Input;
Expand Down Expand Up @@ -78,11 +79,14 @@ public abstract class VersionCatalogUpdate extends DefaultTask {
@Input
public abstract Property<Boolean> getAllowMajorUpdates();

@Input
public abstract MapProperty<String, String> getRejectedVersionsPerModule();

@TaskAction
void updateCatalogs() throws IOException, InterruptedException {
Set<File> catalogs = getCatalogsDirectory().getAsFileTree()
.matching(pattern -> pattern.include("*.versions.toml"))
.getFiles();
.matching(pattern -> pattern.include("*.versions.toml"))
.getFiles();
File outputDir = getOutputDirectory().getAsFile().get();
File logFile = getOutputDirectory().file("updates.log").get().getAsFile();
if (outputDir.isDirectory() || outputDir.mkdirs()) {
Expand All @@ -100,11 +104,11 @@ void updateCatalogs() throws IOException, InterruptedException {

private static boolean supportsUpdate(RichVersion richVersion) {
return richVersion != null
&& richVersion.getRequire() != null
&& richVersion.getStrictly() == null
&& richVersion.getPrefer() == null
&& !richVersion.isRejectAll()
&& richVersion.getRejectedVersions() == null;
&& richVersion.getRequire() != null
&& richVersion.getStrictly() == null
&& richVersion.getPrefer() == null
&& !richVersion.isRejectAll()
&& richVersion.getRejectedVersions() == null;
}

private void updateCatalog(File inputCatalog, File outputCatalog, File logFile) throws IOException, InterruptedException {
Expand All @@ -124,20 +128,30 @@ private void updateCatalog(File inputCatalog, File outputCatalog, File logFile)
detachedConfiguration.setCanBeResolved(true);
detachedConfiguration.setTransitive(false);
detachedConfiguration.getResolutionStrategy()
.cacheDynamicVersionsFor(0, TimeUnit.MINUTES);
.cacheDynamicVersionsFor(0, TimeUnit.MINUTES);
List<Pattern> rejectedQualifiers = getRejectedQualifiers().get()
.stream()
.map(qualifier -> Pattern.compile("(?i).*[.-]" + qualifier + "[.\\d-+]*"))
.collect(Collectors.toList());
.stream()
.map(qualifier -> Pattern.compile("(?i).*[.-]" + qualifier + "[.\\d-+]*"))
.toList();
var rejectedVersionsPerModule = getRejectedVersionsPerModule().get();
detachedConfiguration.getResolutionStrategy().getComponentSelection().all(rules -> {
ModuleComponentIdentifier candidateModule = rules.getCandidate();
String candidateVersion = candidateModule.getVersion();
var moduleIdentifier = candidateModule.getModuleIdentifier();
rejectedQualifiers.forEach(qualifier -> {
if (qualifier.matcher(candidateVersion).find()) {
rules.reject("Rejecting qualifier " + qualifier);
log.println("Rejecting " + candidateModule.getModuleIdentifier() + " version " + candidateVersion + " because of qualifier '" + qualifier + "'");
log.println("Rejecting " + moduleIdentifier + " version " + candidateVersion + " because of qualifier '" + qualifier + "'");
}
});
var rejected = rejectedVersionsPerModule.get(moduleIdentifier.toString());
if (rejected != null) {
var exclusion = Pattern.compile(rejected);
if (exclusion.matcher(candidateVersion).find()) {
rules.reject("Rejecting version " + candidateVersion + " because of configuration. It matches regular expression: " + rejected);
log.println("Rejecting version " + candidateVersion + " because of configuration. It matches regular expression: " + rejected);
}
}
if (!allowMajorUpdate) {
model.findLibrary(
candidateModule.getGroup(), candidateModule.getModule()
Expand Down Expand Up @@ -165,82 +179,82 @@ private void updateCatalog(File inputCatalog, File outputCatalog, File logFile)
Set<String> ignoredModules = getIgnoredModules().get();

model.getLibrariesTable()
.stream()
.filter(library -> !ignoredModules.contains(library.getModule()))
.filter(library -> library.getVersion().getReference() != null || !requiredVersionOf(library).isEmpty())
.map(library -> requirePom(dependencies, library))
.forEach(dependency -> detachedConfiguration.getDependencies().add(dependency));
.stream()
.filter(library -> !ignoredModules.contains(library.getModule()))
.filter(library -> library.getVersion().getReference() != null || !requiredVersionOf(library).isEmpty())
.map(library -> requirePom(dependencies, library))
.forEach(dependency -> detachedConfiguration.getDependencies().add(dependency));

ResolutionResult resolutionResult = detachedConfiguration.getIncoming()
.getResolutionResult();
.getResolutionResult();
resolutionResult
.allComponents(result -> {
ModuleVersionIdentifier mid = result.getModuleVersion();
String latest = mid.getVersion();
Status targetStatus = Status.detectStatus(latest);
log.println("Latest release of " + mid.getModule() + " is " + latest + " (status " + targetStatus + ")");
model.findLibrary(mid.getGroup(), mid.getName()).ifPresent(library -> {
VersionModel version = library.getVersion();
String reference = version.getReference();
if (reference != null) {
model.findVersion(reference).ifPresent(referencedVersion -> {
RichVersion richVersion = referencedVersion.getVersion();
if (supportsUpdate(richVersion)) {
String require = richVersion.getRequire();
Status sourceStatus = Status.detectStatus(require);
if (!Objects.equals(require, latest) && targetStatus.isAsStableOrMoreStableThan(sourceStatus)) {
log.println("Updating required version from " + require + " to " + latest);
String lookup = "(" + reference + "\\s*=\\s*[\"'])(.+?)([\"'])";
int lineNb = referencedVersion.getPosition().line() - 1;
String line = lines.get(lineNb);
Matcher m = Pattern.compile(lookup).matcher(line);
if (m.find()) {
lines.set(lineNb, m.replaceAll("$1" + latest + "$3"));
} else {
log.println("Line " + lineNb + " contains unsupported notation, automatic updating failed");
}
.allComponents(result -> {
ModuleVersionIdentifier mid = result.getModuleVersion();
String latest = mid.getVersion();
Status targetStatus = Status.detectStatus(latest);
log.println("Latest release of " + mid.getModule() + " is " + latest + " (status " + targetStatus + ")");
model.findLibrary(mid.getGroup(), mid.getName()).ifPresent(library -> {
VersionModel version = library.getVersion();
String reference = version.getReference();
if (reference != null) {
model.findVersion(reference).ifPresent(referencedVersion -> {
RichVersion richVersion = referencedVersion.getVersion();
if (supportsUpdate(richVersion)) {
String require = richVersion.getRequire();
Status sourceStatus = Status.detectStatus(require);
if (!Objects.equals(require, latest) && targetStatus.isAsStableOrMoreStableThan(sourceStatus)) {
log.println("Updating required version from " + require + " to " + latest);
String lookup = "(" + reference + "\\s*=\\s*[\"'])(.+?)([\"'])";
int lineNb = referencedVersion.getPosition().line() - 1;
String line = lines.get(lineNb);
Matcher m = Pattern.compile(lookup).matcher(line);
if (m.find()) {
lines.set(lineNb, m.replaceAll("$1" + latest + "$3"));
} else {
log.println("Line " + lineNb + " contains unsupported notation, automatic updating failed");
}
} else {
log.println("Version '" + reference + "' uses a notation which is not supported for automatic upgrades yet.");
}
});
} else {
log.println("Version '" + reference + "' uses a notation which is not supported for automatic upgrades yet.");
}
});
} else {
String lookup = "(version\\s*=\\s*[\"'])(.+?)([\"'])";
int lineNb = library.getPosition().line() - 1;
String line = lines.get(lineNb);
Matcher m = Pattern.compile(lookup).matcher(line);
if (m.find()) {
lines.set(lineNb, m.replaceAll("$1" + latest + "$3"));
} else {
String lookup = "(version\\s*=\\s*[\"'])(.+?)([\"'])";
int lineNb = library.getPosition().line() - 1;
String line = lines.get(lineNb);
Matcher m = Pattern.compile(lookup).matcher(line);
lookup = "(\\s*=\\s*[\"'])(" + library.getGroup() + "):(" + library.getName() + "):(.+?)([\"'])";
m = Pattern.compile(lookup).matcher(line);
if (m.find()) {
lines.set(lineNb, m.replaceAll("$1" + latest + "$3"));
lines.set(lineNb, m.replaceAll("$1$2:$3:" + latest + "$5"));
} else {
lookup = "(\\s*=\\s*[\"'])(" + library.getGroup() + "):("+ library.getName()+"):(.+?)([\"'])";
m = Pattern.compile(lookup).matcher(line);
if (m.find()) {
lines.set(lineNb, m.replaceAll("$1$2:$3:" + latest + "$5"));
} else {
log.println("Line " + lineNb + " contains unsupported notation, automatic updating failed");
}
log.println("Line " + lineNb + " contains unsupported notation, automatic updating failed");
}
}
});
}
});
});

getLogger().lifecycle("Writing updated catalog at " + outputCatalog);
try (PrintWriter writer = newPrintWriter(outputCatalog)) {
lines.forEach(writer::println);
}

String errors = resolutionResult.getAllDependencies()
.stream()
.filter(UnresolvedDependencyResult.class::isInstance)
.map(UnresolvedDependencyResult.class::cast)
.map(r -> {
log.println("Unresolved dependency " + r.getAttempted().getDisplayName());
log.println(" reason " + r.getAttemptedReason());
log.println(" failure" );
r.getFailure().printStackTrace(log);
return "\n - " + r.getAttempted().getDisplayName() + " -> " + r.getFailure().getMessage();
})
.collect(Collectors.joining(""));
.stream()
.filter(UnresolvedDependencyResult.class::isInstance)
.map(UnresolvedDependencyResult.class::cast)
.map(r -> {
log.println("Unresolved dependency " + r.getAttempted().getDisplayName());
log.println(" reason " + r.getAttemptedReason());
log.println(" failure");
r.getFailure().printStackTrace(log);
return "\n - " + r.getAttempted().getDisplayName() + " -> " + r.getFailure().getMessage();
})
.collect(Collectors.joining(""));
if (!errors.isEmpty()) {
throw new GradleException("Some modules couldn't be updated because of the following reasons:" + errors);
}
Expand Down

0 comments on commit 30f106f

Please sign in to comment.