Skip to content

Commit 3db8e80

Browse files
e5LAtimtebeek
andauthored
Add recipe to update SCM tag based on Git origin (#5647)
* Add recipe to update SCM tag based on Git origin * Adding UpdateScmRecipe to maven.BestPractices * Fix cosmetics * Add missing newline at EOF * Fix the test case with git ssh origin * Avoid finding any other `scm` tags * Add `@Value` to ScmValues * Apply formatter * Delete public ScmValues class, add ScmValues as private class of UpdateScmFromGitOrigin * Drop need for `visitDocument` and associated field * No need for conditionals handled by visitor itself * Add backticks around scm in description * Adding extra test cases * Refactor logic to parse gitOrigin and replace only host and path * Retain URL suffix when URLs start with the same prefix * Minimize implementation * Add trailing enter --------- Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent 05a515f commit 3db8e80

File tree

5 files changed

+682
-0
lines changed

5 files changed

+682
-0
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.maven;
17+
18+
import lombok.Value;
19+
import org.jspecify.annotations.Nullable;
20+
import org.openrewrite.ExecutionContext;
21+
import org.openrewrite.Preconditions;
22+
import org.openrewrite.Recipe;
23+
import org.openrewrite.TreeVisitor;
24+
import org.openrewrite.marker.GitProvenance;
25+
import org.openrewrite.maven.search.FindScm;
26+
import org.openrewrite.xml.ChangeTagValueVisitor;
27+
import org.openrewrite.xml.tree.Xml;
28+
29+
import java.util.Optional;
30+
import java.util.regex.Matcher;
31+
import java.util.regex.Pattern;
32+
33+
import static org.openrewrite.internal.StringUtils.isNullOrEmpty;
34+
35+
public class UpdateScmFromGitOrigin extends Recipe {
36+
@Override
37+
public String getDisplayName() {
38+
return "Update SCM section to match Git origin";
39+
}
40+
41+
@Override
42+
public String getDescription() {
43+
return "Updates the Maven `<scm>` section based on the Git remote origin.";
44+
}
45+
46+
@Override
47+
public TreeVisitor<?, ExecutionContext> getVisitor() {
48+
return Preconditions.check(new FindScm(), new MavenVisitor<ExecutionContext>() {
49+
@Override
50+
public Xml visitTag(Xml.Tag tag, ExecutionContext ctx) {
51+
if ("project".equals(tag.getName())) {
52+
return super.visitTag(tag, ctx);
53+
}
54+
if ("scm".equals(tag.getName())) {
55+
Optional.ofNullable(getCursor().firstEnclosing(Xml.Document.class))
56+
.map(Xml.Document::getMarkers)
57+
.flatMap(markers -> markers.findFirst(GitProvenance.class))
58+
.map(GitProvenance::getOrigin)
59+
.map(GitOrigin::parseGitUrl)
60+
.ifPresent(gitOrigin -> {
61+
updateTagValue(tag, "url", gitOrigin);
62+
updateTagValue(tag, "connection", gitOrigin);
63+
updateTagValue(tag, "developerConnection", gitOrigin);
64+
});
65+
return tag;
66+
}
67+
// Only process the <scm> tag if it's a direct child of <project>
68+
return tag;
69+
}
70+
71+
private void updateTagValue(Xml.Tag tag, String tagName, GitOrigin gitOrigin) {
72+
tag.getChild(tagName).ifPresent(childTag -> {
73+
String originalUrl = childTag.getValue().orElse("");
74+
String updatedUrl = gitOrigin.replaceHostAndPath(originalUrl);
75+
doAfterVisit(new ChangeTagValueVisitor<>(childTag, updatedUrl));
76+
});
77+
}
78+
});
79+
}
80+
81+
@Value
82+
static class GitOrigin {
83+
private static final Pattern[] URL_PATTERNS = {
84+
// SSH format: git@host:path(.git)?
85+
Pattern.compile("^git@([^:]+):(.+?)(?:\\.git)?$"),
86+
87+
// HTTP/HTTPS with optional username and port: http(s)://[username@]host[:port]/path(.git)?
88+
Pattern.compile("^https?://(?:[^@]+@)?([^/:]+(?::[0-9]+)?)/(.+?)(?:\\.git)?$"),
89+
90+
// SSH with protocol and port: ssh://git@host[:port]/path(.git)?
91+
Pattern.compile("^ssh://git@([^/:]+(?::[0-9]+)?)/(.+?)(?:\\.git)?$"),
92+
93+
// Generic protocol://[user@]host[:port]/path(.git)? - catches any other protocols
94+
Pattern.compile("^[a-zA-Z][a-zA-Z0-9+.-]*://(?:[^@]+@)?([^/:]+(?::[0-9]+)?)/(.+?)(?:\\.git)?$")
95+
};
96+
97+
String host;
98+
String path;
99+
100+
static @Nullable GitOrigin parseGitUrl(String gitUrl) {
101+
if (!isNullOrEmpty(gitUrl)) {
102+
for (Pattern pattern : URL_PATTERNS) {
103+
Matcher matcher = pattern.matcher(gitUrl);
104+
if (matcher.matches()) {
105+
return new GitOrigin(matcher.group(1), matcher.group(2));
106+
}
107+
}
108+
}
109+
return null;
110+
}
111+
112+
String replaceHostAndPath(String originalUrl) {
113+
if (isNullOrEmpty(originalUrl)) {
114+
return originalUrl;
115+
}
116+
117+
if (originalUrl.startsWith("scm:git:")) {
118+
String actualUrl = originalUrl.substring("scm:git:".length());
119+
return "scm:git:" + replaceHostAndPath(actualUrl);
120+
}
121+
122+
String gitSuffix = originalUrl.endsWith(".git") ? ".git" : "";
123+
if (originalUrl.startsWith("git@")) {
124+
return "git@" + host + ":" + path + gitSuffix;
125+
}
126+
127+
Matcher protocolMatcher = Pattern.compile("^([a-zA-Z][a-zA-Z0-9+.-]*://)(?:([^@/]+)@)?").matcher(originalUrl);
128+
if (protocolMatcher.find()) {
129+
String protocol = protocolMatcher.group(1);
130+
String user = protocolMatcher.group(2);
131+
String userPrefix = (user != null) ? user + "@" : "";
132+
String newUrl = protocol + userPrefix + host + "/" + path + gitSuffix;
133+
if (originalUrl.startsWith(newUrl)) {
134+
return originalUrl; // Retain e.g. tree/${project.scm.tag}
135+
}
136+
return newUrl;
137+
}
138+
139+
// Return the original URL if no patterns matched
140+
return originalUrl;
141+
}
142+
}
143+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.maven.search;
17+
18+
import org.openrewrite.ExecutionContext;
19+
import org.openrewrite.Recipe;
20+
import org.openrewrite.TreeVisitor;
21+
import org.openrewrite.marker.SearchResult;
22+
import org.openrewrite.maven.MavenVisitor;
23+
import org.openrewrite.xml.tree.Xml;
24+
25+
public class FindScm extends Recipe {
26+
@Override
27+
public String getDisplayName() {
28+
return "Find SCM tag";
29+
}
30+
31+
@Override
32+
public String getDescription() {
33+
return "Finds any `<scm>` tag directly inside the `<project>` root of a Maven pom.xml file.";
34+
}
35+
36+
@Override
37+
public TreeVisitor<?, ExecutionContext> getVisitor() {
38+
return new MavenVisitor<ExecutionContext>() {
39+
@Override
40+
public Xml visitTag(Xml.Tag tag, ExecutionContext ctx) {
41+
if ("project".equals(tag.getName())) {
42+
return super.visitTag(tag, ctx);
43+
} else if ("scm".equals(tag.getName())) {
44+
return SearchResult.found(tag);
45+
}
46+
return tag;
47+
}
48+
};
49+
}
50+
}

rewrite-maven/src/main/resources/META-INF/rewrite/maven.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ recipeList:
2626
- org.openrewrite.maven.RemoveDuplicateDependencies
2727
- org.openrewrite.maven.RemoveRedundantDependencyVersions
2828
- org.openrewrite.maven.plugin.DependencyPluginGoalResolveSources
29+
- org.openrewrite.maven.UpdateScmFromGitOrigin
2930
---
3031
type: specs.openrewrite.org/v1beta/recipe
3132
name: org.openrewrite.maven.cleanup.PrefixlessExpressions

0 commit comments

Comments
 (0)