RepositoryKey.java
package network.ike.workspace.cascade;
import org.apache.maven.api.model.Scm;
/**
* The identity of a release-cascade node — a repository, named by
* its {@code <scm>} URL (IKE-Network/ike-issues#496 part C).
*
* <p>A release node is a repository, not a coordinate. A single
* reactor produces many coordinates ({@code ike-tooling} alone
* produces {@code ike-maven-plugin},
* {@code ike-build-standards}, {@code ike-workspace-model}, and
* more) but the cascade releases the repository as one unit. The
* join key that collapses many coordinates onto one node is the
* {@code <scm>} URL each coordinate inherits from its reactor-root
* POM. {@code RepositoryKey} is that join key.
*
* <p>SCM URLs arrive in many syntactic forms — {@code scm:git:https://}
* prefixed, plain {@code https://}, {@code git@host:owner/repo}
* SSH shorthand, {@code ssh://} explicit, with or without a trailing
* {@code .git}. {@code RepositoryKey} canonicalises every form to a
* single normalised {@code https://host/owner/repo} string, so two
* keys derived from different syntactic variants of the same URL
* compare equal.
*
* @param url the canonical {@code https://host/owner/repo} form of
* the SCM URL
*/
public record RepositoryKey(String url) {
/**
* Canonical constructor — validates and normalises the URL.
*/
public RepositoryKey {
if (url == null || url.isBlank()) {
throw new IllegalArgumentException(
"RepositoryKey url is required");
}
url = normalise(url);
}
/**
* Builds a key from a Maven {@link Scm} block, preferring
* {@code <url>} over {@code <connection>} when both are
* declared.
*
* @param scm the SCM block; may be {@code null}
* @return the key, or {@code null} when {@code scm} is
* {@code null} or declares neither a URL nor a
* connection
*/
public static RepositoryKey fromScm(Scm scm) {
if (scm == null) {
return null;
}
String url = scm.getUrl();
if (url == null || url.isBlank()) {
url = scm.getConnection();
}
if (url == null || url.isBlank()) {
return null;
}
return new RepositoryKey(url);
}
/**
* Builds a key from any syntactic form of an SCM URL.
*
* @param scmUrl the SCM URL or connection string; must not be
* {@code null} or blank
* @return the canonicalised key
* @throws IllegalArgumentException if {@code scmUrl} is null or
* blank
*/
public static RepositoryKey of(String scmUrl) {
return new RepositoryKey(scmUrl);
}
/**
* Normalises any common Git URL form to
* {@code https://host/owner/repo}.
*
* <p>Handles: Maven {@code scm:provider:} prefixes,
* {@code git@host:path} SSH shorthand, {@code ssh://} URLs,
* trailing {@code .git}, and trailing slashes.
*/
private static String normalise(String raw) {
String s = raw.trim();
// Strip Maven SCM provider prefix — "scm:git:", "scm:hg:", etc.
if (s.startsWith("scm:")) {
int colon = s.indexOf(':', 4);
s = colon >= 0 ? s.substring(colon + 1) : s.substring(4);
}
// Convert SSH shorthand "git@host:owner/repo" → "https://host/owner/repo"
if (s.startsWith("git@") && !s.contains("://")) {
int colon = s.indexOf(':', 4);
if (colon > 0) {
String host = s.substring(4, colon);
String path = s.substring(colon + 1);
s = "https://" + host + "/" + path;
}
}
// Convert "ssh://git@host/path" or "ssh://host/path" → "https://host/path"
if (s.startsWith("ssh://git@")) {
s = "https://" + s.substring("ssh://git@".length());
} else if (s.startsWith("ssh://")) {
s = "https://" + s.substring("ssh://".length());
}
// Drop trailing slashes BEFORE the ".git" check, so inputs
// like "...ike-tooling.git/" canonicalise correctly.
while (s.endsWith("/")) {
s = s.substring(0, s.length() - 1);
}
// Drop a trailing ".git".
if (s.endsWith(".git")) {
s = s.substring(0, s.length() - 4);
}
return s;
}
}