Skip to content

Commit

Permalink
Maintenance score based on commit frequency
Browse files Browse the repository at this point in the history
  • Loading branch information
juulhobert committed Nov 24, 2024
1 parent 899a413 commit f053b0e
Show file tree
Hide file tree
Showing 27 changed files with 1,821 additions and 43 deletions.
65 changes: 22 additions & 43 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
<maven.compiler.release>21</maven.compiler.release>

<jackson-jr-objects.version>2.18.0</jackson-jr-objects.version>
<jackson.version>2.18.1</jackson.version>
<junit-jupiter.version>5.11.2</junit-jupiter.version>
<assertj-core.version>3.26.3</assertj-core.version>
<mockito-junit-jupiter.version>5.14.2</mockito-junit-jupiter.version>
Expand All @@ -70,7 +70,6 @@
<slf4j.version>2.0.16</slf4j.version>
</properties>


<dependencyManagement>
<dependencies>
<dependency>
Expand Down Expand Up @@ -127,6 +126,14 @@
<version>${maven-dependencies.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${jackson.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>

Expand All @@ -143,13 +150,14 @@
<version>${maven-plugin-tools.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.jr</groupId>
<artifactId>jackson-jr-objects</artifactId>
<version>${jackson-jr-objects.version}</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.jr</groupId>
<artifactId>jackson-jr-annotation-support</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
Expand Down Expand Up @@ -255,6 +263,14 @@
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
Expand Down Expand Up @@ -370,42 +386,5 @@
</plugins>
</build>
</profile>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.jreleaser</groupId>
<artifactId>jreleaser-maven-plugin</artifactId>
<configuration>
<jreleaser>
<signing>
<active>ALWAYS</active>
<armored>true</armored>
</signing>
<release>
<github>
<owner>Giovds</owner>
<name>${project.artifactId}</name>
<overwrite>true</overwrite>
</github>
</release>
<deploy>
<maven>
<mavenCentral>
<sonatype>
<active>ALWAYS</active>
<url>https://central.sonatype.com/api/v1/publisher</url>
<stagingRepositories>target/staging-deploy</stagingRepositories>
</sonatype>
</mavenCentral>
</maven>
</deploy>
</jreleaser>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
104 changes: 104 additions & 0 deletions src/main/java/com/giovds/PomClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.giovds;

import com.giovds.dto.PomResponse;
import com.giovds.dto.Scm;
import org.apache.maven.plugin.logging.Log;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;

public class PomClient implements PomClientInterface {

private final String basePath;
private final String pomPathTemplate;

private final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();

private final HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();

private final Log log;

public PomClient(Log log) {
this("https://repo1.maven.org", "/maven2/%s/%s/%s/%s-%s.pom", log);
}

public PomClient(String basePath, String pomPathTemplate, Log log) {
this.basePath = basePath;
this.pomPathTemplate = pomPathTemplate;
this.log = log;
}

public PomResponse getPom(String group, String artifact, String version) throws IOException, InterruptedException {
final String path = String.format(pomPathTemplate, group.replace(".", "/"), artifact, version, artifact, version);
final HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(URI.create(basePath + path))
.build();

return client.send(request, new PomResponseBodyHandler()).body();
}

private class PomResponseBodyHandler implements HttpResponse.BodyHandler<PomResponse> {

@Override
public HttpResponse.BodySubscriber<PomResponse> apply(final HttpResponse.ResponseInfo responseInfo) {
int statusCode = responseInfo.statusCode();

if (statusCode < 200 || statusCode >= 300) {
return HttpResponse.BodySubscribers.mapping(HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8), s -> {
throw new RuntimeException("Search failed: status: %d body: %s".formatted(responseInfo.statusCode(), s));
});
}

HttpResponse.BodySubscriber<InputStream> stream = HttpResponse.BodySubscribers.ofInputStream();

return HttpResponse.BodySubscribers.mapping(stream, this::toPomResponse);
}

private PomResponse toPomResponse(final InputStream inputStream) {
try (final InputStream input = inputStream) {
DocumentBuilder documentBuilder = PomClient.this.documentBuilderFactory.newDocumentBuilder();
Document doc = documentBuilder.parse(input);

doc.getDocumentElement().normalize();

Element root = doc.getDocumentElement();
NodeList urlNodes = root.getElementsByTagName("url");

if (urlNodes.getLength() == 0) {
return PomResponse.empty();
}
String url = urlNodes.item(0).getTextContent();

Scm scm = Scm.empty();
NodeList scmNodes = root.getElementsByTagName("scm");
if (scmNodes.getLength() > 0) {
Element scmElement = (Element) scmNodes.item(0);
NodeList scmUrlNodes = scmElement.getElementsByTagName("url");
if (scmUrlNodes.getLength() > 0) {
String scmUrl = scmUrlNodes.item(0).getTextContent();
scm = new Scm(scmUrl);
}
}

return new PomResponse(url, scm);
} catch (IOException | ParserConfigurationException | SAXException e) {
throw new RuntimeException(e);
}
}
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/giovds/PomClientInterface.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.giovds;

import com.giovds.dto.PomResponse;

import java.io.IOException;

public interface PomClientInterface {
PomResponse getPom(String group, String artifact, String version) throws IOException, InterruptedException;
}
112 changes: 112 additions & 0 deletions src/main/java/com/giovds/UnmaintainedMojo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.giovds;

import com.giovds.collector.github.GithubCollector;
import com.giovds.collector.github.GithubGuesser;
import com.giovds.dto.PomResponse;
import com.giovds.dto.github.internal.Collected;
import com.giovds.evaluator.MaintenanceEvaluator;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugin.logging.SystemStreamLog;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

@Mojo(name = "unmaintained",
defaultPhase = LifecyclePhase.TEST_COMPILE,
requiresOnline = true,
requiresDependencyResolution = ResolutionScope.TEST)
public class UnmaintainedMojo extends AbstractMojo {

private final PomClientInterface client;
private final GithubGuesser githubGuesser;
private final GithubCollector githubCollector;
private final MaintenanceEvaluator maintenanceEvaluator;

@Parameter(readonly = true, required = true, defaultValue = "${project}")
private MavenProject project;

/**
* Required for initialization by Maven
*/
public UnmaintainedMojo() {
this(new SystemStreamLog());
}

public UnmaintainedMojo(Log log) {
this(new PomClient(log), new GithubGuesser(), new GithubCollector(log), new MaintenanceEvaluator());
}

public UnmaintainedMojo(
final PomClientInterface client,
final GithubGuesser githubGuesser,
final GithubCollector githubCollector,
final MaintenanceEvaluator maintenanceEvaluator) {
this.client = client;
this.githubGuesser = githubGuesser;
this.githubCollector = githubCollector;
this.maintenanceEvaluator = maintenanceEvaluator;
}

@Override
public void execute() throws MojoFailureException {
final List<Dependency> dependencies = project.getDependencies();

if (dependencies.isEmpty()) {
// When building a POM without any dependencies there will be nothing to query.
return;
}

final Map<Dependency, PomResponse> pomResponses = dependencies.stream()
.map(dependency -> {
try {
PomResponse pomResponse = client.getPom(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion());

return new DependencyPomResponsePair(dependency, pomResponse);
} catch (Exception e) {
getLog().error("Failed to fetch POM for %s:%s:%s".formatted(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion()), e);
return new DependencyPomResponsePair(dependency, PomResponse.empty());
}
})
.collect(Collectors.toMap(DependencyPomResponsePair::dependency, DependencyPomResponsePair::pomResponse));

for (Dependency dependency : pomResponses.keySet()) {
final PomResponse pomResponse = pomResponses.get(dependency);
final String projectUrl = pomResponse.url();
final String projectScmUrl = pomResponse.scmUrl();

// First try to get the Github owner and repo from the url otherwise try to get it from the SCM url
var guess = projectUrl != null ? githubGuesser.guess(projectUrl) : null;
if (guess == null && projectScmUrl != null) {
guess = githubGuesser.guess(projectScmUrl);
}

if (guess == null) {
getLog().warn("Could not guess Github owner and repo for %s".formatted(dependency.getManagementKey()));
continue;
}

Collected collected;
try {
collected = githubCollector.collect(guess.owner(), guess.repo());
} catch (ExecutionException | InterruptedException e) {
throw new MojoFailureException("Failed to collect Github data for %s".formatted(dependency.getManagementKey()), e);
}

double score = maintenanceEvaluator.evaluateCommitsFrequency(collected);
getLog().info("Maintenance score for %s: %f".formatted(dependency.getManagementKey(), score));
}
}

private record DependencyPomResponsePair(Dependency dependency, PomResponse pomResponse) {
}
}
Loading

0 comments on commit f053b0e

Please sign in to comment.