Skip to content

Commit

Permalink
Add Git commit hash version comparing
Browse files Browse the repository at this point in the history
  • Loading branch information
xpple committed Jul 25, 2024
1 parent d9a0006 commit c02641a
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import net.fabricmc.loader.api.SemanticVersion;
import net.fabricmc.loader.api.Version;
import net.fabricmc.loader.impl.util.version.CommitHashVersion;
import net.fabricmc.loader.impl.util.version.SemanticVersionImpl;

public enum VersionComparisonOperator {
Expand All @@ -28,6 +29,11 @@ public boolean test(SemanticVersion a, SemanticVersion b) {
return a.compareTo((Version) b) >= 0;
}

@Override
public boolean test(CommitHashVersion a, CommitHashVersion b) {
return a.compareTo(b) >= 0;
}

@Override
public SemanticVersion minVersion(SemanticVersion version) {
return version;
Expand All @@ -39,6 +45,11 @@ public boolean test(SemanticVersion a, SemanticVersion b) {
return a.compareTo((Version) b) <= 0;
}

@Override
public boolean test(CommitHashVersion a, CommitHashVersion b) {
return a.compareTo(b) <= 0;
}

@Override
public SemanticVersion maxVersion(SemanticVersion version) {
return version;
Expand All @@ -50,6 +61,11 @@ public boolean test(SemanticVersion a, SemanticVersion b) {
return a.compareTo((Version) b) > 0;
}

@Override
public boolean test(CommitHashVersion a, CommitHashVersion b) {
return a.compareTo(b) > 0;
}

@Override
public SemanticVersion minVersion(SemanticVersion version) {
return version;
Expand All @@ -61,6 +77,11 @@ public boolean test(SemanticVersion a, SemanticVersion b) {
return a.compareTo((Version) b) < 0;
}

@Override
public boolean test(CommitHashVersion a, CommitHashVersion b) {
return a.compareTo(b) < 0;
}

@Override
public SemanticVersion maxVersion(SemanticVersion version) {
return version;
Expand All @@ -72,6 +93,11 @@ public boolean test(SemanticVersion a, SemanticVersion b) {
return a.compareTo((Version) b) == 0;
}

@Override
public boolean test(CommitHashVersion a, CommitHashVersion b) {
return a.compareTo(b) == 0;
}

@Override
public SemanticVersion minVersion(SemanticVersion version) {
return version;
Expand All @@ -90,6 +116,11 @@ public boolean test(SemanticVersion a, SemanticVersion b) {
&& a.getVersionComponent(1) == b.getVersionComponent(1);
}

@Override
public boolean test(CommitHashVersion a, CommitHashVersion b) {
throw new UnsupportedOperationException("This operator is not supported for Git hash string versions");
}

@Override
public SemanticVersion minVersion(SemanticVersion version) {
return version;
Expand All @@ -107,6 +138,11 @@ public boolean test(SemanticVersion a, SemanticVersion b) {
&& a.getVersionComponent(0) == b.getVersionComponent(0);
}

@Override
public boolean test(CommitHashVersion a, CommitHashVersion b) {
throw new UnsupportedOperationException("This operator is not supported for Git hash string versions");
}

@Override
public SemanticVersion minVersion(SemanticVersion version) {
return version;
Expand Down Expand Up @@ -143,6 +179,8 @@ public final boolean isMaxInclusive() {
public final boolean test(Version a, Version b) {
if (a instanceof SemanticVersion && b instanceof SemanticVersion) {
return test((SemanticVersion) a, (SemanticVersion) b);
} else if (a instanceof CommitHashVersion && b instanceof CommitHashVersion) {
return test((CommitHashVersion) a, (CommitHashVersion) b);
} else if (minInclusive || maxInclusive) {
return a.getFriendlyString().equals(b.getFriendlyString());
} else {
Expand All @@ -152,6 +190,8 @@ public final boolean test(Version a, Version b) {

public abstract boolean test(SemanticVersion a, SemanticVersion b);

public abstract boolean test(CommitHashVersion a, CommitHashVersion b);

public SemanticVersion minVersion(SemanticVersion version) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ static LoaderModMetadata parse(JsonReader reader) throws IOException, ParseMetad
}

try {
version = VersionParser.parse(reader.nextString(), false);
version = VersionParser.parse(reader.nextString(), false, contact);
} catch (VersionParsingException e) {
throw new ParseMetadataException("Failed to parse version", e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package net.fabricmc.loader.impl.util.version;

import net.fabricmc.loader.api.Version;
import net.fabricmc.loader.api.VersionParsingException;
import net.fabricmc.loader.impl.lib.gson.JsonReader;

import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CommitHashVersion implements Version {
private static final Pattern GIT_COMMIT_HASH_PATTERN = Pattern.compile("^[0-9a-fA-F]{40}$");
private static final Pattern SOURCES_PATTERN = Pattern.compile("^(?:https?://)?github\\.com/([\\w-]+)/([\\w-]+)/?$");

private final String commitHash;
private final Instant commitDate;

public CommitHashVersion(String commitHash, String sources) throws VersionParsingException {
if (!GIT_COMMIT_HASH_PATTERN.matcher(commitHash).matches()) {
throw new VersionParsingException("Invalid Git commit hash");
}

this.commitHash = commitHash;

Matcher matcher = SOURCES_PATTERN.matcher(sources);
if (!matcher.matches()) {
throw new VersionParsingException("Unsupported or invalid sources link");
}

String apiUrlString = String.format("https://api.github.com/repos/%s/%s/git/commits/%s", matcher.group(1), matcher.group(2), this.commitHash);
URL apiUrl;
try {
apiUrl = new URL(apiUrlString);
} catch (MalformedURLException e) {
throw new VersionParsingException("Sources URL is malformed");
}

String dateString;
try (JsonReader reader = new JsonReader(new InputStreamReader(apiUrl.openStream()))) {
reader.beginObject();

// skip sha, node id, url, HTML url, author
for (int i = 0; i < 5; i++) {
reader.nextName();
reader.skipValue();
}

reader.nextName();
reader.beginObject();
// skip name, email
for (int i = 0; i < 2; i++) {
reader.nextName();
reader.skipValue();
}
reader.nextName();
dateString = reader.nextString();
} catch (IOException e) {
throw new VersionParsingException("Could not connect to GitHub's API");
}

Instant date;
try {
date = Instant.parse(dateString);
} catch (DateTimeParseException e) {
throw new VersionParsingException("Date could not be parsed");
}

this.commitDate = date;
}

@Override
public String getFriendlyString() {
return String.format("%s (%s)", this.commitHash.substring(0, 7), this.commitDate.toString());
}

@Override
public int compareTo(@NotNull Version other) {
if (!(other instanceof CommitHashVersion)) {
return this.getFriendlyString().compareTo(other.getFriendlyString());
}

return this.commitDate.compareTo(((CommitHashVersion) other).commitDate);
}

@Override
public boolean equals(Object obj) {
if (obj instanceof CommitHashVersion) {
return this.commitHash.equals(((CommitHashVersion) obj).commitHash);
}

return false;
}

@Override
public String toString() {
return this.getFriendlyString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@
import net.fabricmc.loader.api.SemanticVersion;
import net.fabricmc.loader.api.Version;
import net.fabricmc.loader.api.VersionParsingException;
import net.fabricmc.loader.api.metadata.ContactInformation;

public final class VersionParser {
public static Version parse(String s, boolean storeX) throws VersionParsingException {
return parse(s, storeX, null);
}

public static Version parse(String s, boolean storeX, ContactInformation contact) throws VersionParsingException {
if (s == null || s.isEmpty()) {
throw new VersionParsingException("Version must be a non-empty string!");
}
Expand All @@ -31,7 +36,16 @@ public static Version parse(String s, boolean storeX) throws VersionParsingExcep
try {
version = new SemanticVersionImpl(s, storeX);
} catch (VersionParsingException e) {
version = new StringVersion(s);
String sources;
if (contact != null && (sources = contact.get("sources").orElse(null)) != null) {
try {
version = new CommitHashVersion(s, sources);
} catch (VersionParsingException ex) {
version = new StringVersion(s);
}
} else {
version = new StringVersion(s);
}
}

return version;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import net.fabricmc.loader.api.SemanticVersion;
import net.fabricmc.loader.api.Version;
import net.fabricmc.loader.api.VersionParsingException;
import net.fabricmc.loader.api.metadata.ContactInformation;
import net.fabricmc.loader.api.metadata.version.VersionComparisonOperator;
import net.fabricmc.loader.api.metadata.version.VersionInterval;
import net.fabricmc.loader.api.metadata.version.VersionPredicate;
Expand All @@ -36,6 +37,10 @@ public final class VersionPredicateParser {
private static final VersionComparisonOperator[] OPERATORS = VersionComparisonOperator.values();

public static VersionPredicate parse(String predicate) throws VersionParsingException {
return parse(predicate, null);
}

public static VersionPredicate parse(String predicate, ContactInformation contact) throws VersionParsingException {
List<SingleVersionPredicate> predicateList = new ArrayList<>();

for (String s : predicate.split(" ")) {
Expand All @@ -55,7 +60,7 @@ public static VersionPredicate parse(String predicate) throws VersionParsingExce
}
}

Version version = VersionParser.parse(s, true);
Version version = VersionParser.parse(s, true, contact);

if (version instanceof SemanticVersion) {
SemanticVersion semVer = (SemanticVersion) version;
Expand All @@ -80,10 +85,12 @@ public static VersionPredicate parse(String predicate) throws VersionParsingExce

version = new SemanticVersionImpl(newComponents, "", semVer.getBuildKey().orElse(null));
}
} else if (!operator.isMinInclusive() && !operator.isMaxInclusive()) { // non-semver without inclusive bound
throw new VersionParsingException("Invalid predicate: "+predicate+", version ranges need to be semantic version compatible to use operators that exclude the bound!");
} else { // non-semver with inclusive bound
operator = VersionComparisonOperator.EQUAL;
} else if (!(version instanceof CommitHashVersion)) {
if (!operator.isMinInclusive() && !operator.isMaxInclusive()) { // unknown format without inclusive bound
throw new VersionParsingException("Invalid predicate: "+predicate+", version ranges need to be comparable to use operators that exclude the bound!");
} else { // unknown format with inclusive bound
operator = VersionComparisonOperator.EQUAL;
}
}

predicateList.add(new SingleVersionPredicate(operator, version));
Expand Down
51 changes: 51 additions & 0 deletions src/test/java/net/fabricmc/test/VersionParsingTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@

package net.fabricmc.test;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;

import net.fabricmc.loader.api.metadata.ContactInformation;
import net.fabricmc.loader.impl.metadata.ContactInformationImpl;
import net.fabricmc.loader.impl.util.version.CommitHashVersion;

import org.jetbrains.annotations.Nullable;

import net.fabricmc.loader.api.Version;
Expand All @@ -35,6 +41,15 @@ private static Exception tryParseSemantic(String s, boolean storeX) {
}
}

private static Exception tryParseCommitHash(String s, String sources) {
try {
new CommitHashVersion(s, sources);
return null;
} catch (VersionParsingException e) {
return e;
}
}

private static void testTrue(@Nullable Exception b) {
if (b != null) {
throw new RuntimeException("Test failed!", b);
Expand Down Expand Up @@ -318,5 +333,41 @@ public static void main(String[] args) throws Exception {
testFalse(predicate.test(new SemanticVersionImpl("2.0.0", false)));
testFalse(predicate.test(new SemanticVersionImpl("2.0.0-beta.2", false)));
}

// Test: Commit Hash versions
testTrue(tryParseCommitHash("d9a000670180e73569a6983c423dd4299278afda", "https://github.com/fabricMC/fabric-loader/"));
testTrue(tryParseCommitHash("d9a000670180e73569a6983c423dd4299278afda", "https://github.com/fabricMC/fabric-loader"));
testTrue(tryParseCommitHash("d9a000670180e73569a6983c423dd4299278afda", "http://github.com/fabricMC/fabric-loader/"));
testTrue(tryParseCommitHash("d9a000670180e73569a6983c423dd4299278afda", "github.com/fabricMC/fabric-loader/"));

// Test: Comparing Commit Hash versions.
Map<String, String> contactMap = new HashMap<String, String>() {{
put("sources", "https://github.com/fabricMC/fabric-loader/");
}};
ContactInformation contact = new ContactInformationImpl(contactMap);
{
Predicate<Version> predicate = VersionPredicateParser.parse("d9a000670180e73569a6983c423dd4299278afda", contact);
testTrue(predicate.test(new CommitHashVersion("d9a000670180e73569a6983c423dd4299278afda", "https://github.com/fabricMC/fabric-loader/")));
}
{
Predicate<Version> predicate = VersionPredicateParser.parse(">=4dbcb72dcce4e4034f58ed3839da41aae097c901", contact);
testTrue(predicate.test(new CommitHashVersion("d9a000670180e73569a6983c423dd4299278afda", "https://github.com/fabricMC/fabric-loader/")));
}
{
Predicate<Version> predicate = VersionPredicateParser.parse("<4dbcb72dcce4e4034f58ed3839da41aae097c901", contact);
testFalse(predicate.test(new CommitHashVersion("d9a000670180e73569a6983c423dd4299278afda", "https://github.com/fabricMC/fabric-loader/")));
}

// Test: Unsupported Commit Hash version predicates
try {
VersionPredicateParser.parse("~4dbcb72dcce4e4034f58ed3839da41aae097c901", contact);
} catch (UnsupportedOperationException e) {
testFalse(e);
}
try {
VersionPredicateParser.parse("^4dbcb72dcce4e4034f58ed3839da41aae097c901", contact);
} catch (UnsupportedOperationException e) {
testFalse(e);
}
}
}

0 comments on commit c02641a

Please # to comment.