diff --git a/src/main/java/hudson/plugins/git/GitSCM.java b/src/main/java/hudson/plugins/git/GitSCM.java index 9b609fcbfd..1ad75e2d30 100644 --- a/src/main/java/hudson/plugins/git/GitSCM.java +++ b/src/main/java/hudson/plugins/git/GitSCM.java @@ -2007,13 +2007,16 @@ public BuildData copyBuildData(Run build) { BuildData base = getBuildData(build); ScmName sn = getExtensions().get(ScmName.class); String scmName = sn == null ? null : sn.getName(); - if (base==null) - return new BuildData(scmName, getUserRemoteConfigs()); - else { - BuildData buildData = base.clone(); + BuildData buildData; + if (base==null) { + buildData = new BuildData(scmName, getUserRemoteConfigs()); + } else { + buildData = base.clone(); buildData.setScmName(scmName); return buildData; } + buildData.setBrowser(getBrowser()); + return buildData; } /** diff --git a/src/main/java/hudson/plugins/git/browser/GitRepositoryBrowser.java b/src/main/java/hudson/plugins/git/browser/GitRepositoryBrowser.java index 91d5c45bd3..55052293ac 100644 --- a/src/main/java/hudson/plugins/git/browser/GitRepositoryBrowser.java +++ b/src/main/java/hudson/plugins/git/browser/GitRepositoryBrowser.java @@ -45,7 +45,7 @@ public final String getRepoUrl() { return url; } - public final URL getUrl() throws IOException { + public URL getUrl() throws IOException { String u = url; StaplerRequest2 req = Stapler.getCurrentRequest2(); if (req != null) { @@ -107,6 +107,17 @@ public URL getChangeSetLink(final String commitId) throws IOException { return null; } + /** + * Determines the link to the given ref. + * + * @param ref The specific ref to link to + * @return null if the browser doesn't have any meaningful URL for a ref + */ + @CheckForNull + public URL getRefLink(String ref) throws IOException { + return null; + } + /** * Determines whether a URL should be normalized * Overridden in the rare case where it shouldn't diff --git a/src/main/java/hudson/plugins/git/browser/GithubWeb.java b/src/main/java/hudson/plugins/git/browser/GithubWeb.java index 133aca461a..a24fa399b5 100644 --- a/src/main/java/hudson/plugins/git/browser/GithubWeb.java +++ b/src/main/java/hudson/plugins/git/browser/GithubWeb.java @@ -36,6 +36,17 @@ public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { return new URL(url, url.getPath()+"commit/" + changeSet.getId()); } + /** + * Determines the link to the given ref. + * @param ref The specific ref to link to + * @return null if the browser doesn't have any meaningful URL for a ref + */ + @Override + public URL getRefLink(String ref) throws IOException { + URL url = getUrl(); + return new URL(url, url.getPath() + "tree/" + ref); + } + /** * Creates a link to the file diff. * http://[GitHib URL]/commit/573670a3bb1f3b939e87f1dee3e99b6bfe281fcb#diff-N diff --git a/src/main/java/hudson/plugins/git/util/BuildData.java b/src/main/java/hudson/plugins/git/util/BuildData.java index 63c44ea57e..2f2d35f2fa 100644 --- a/src/main/java/hudson/plugins/git/util/BuildData.java +++ b/src/main/java/hudson/plugins/git/util/BuildData.java @@ -9,9 +9,12 @@ import hudson.plugins.git.Branch; import hudson.plugins.git.Revision; import hudson.plugins.git.UserRemoteConfig; +import hudson.plugins.git.browser.GitRepositoryBrowser; +import java.io.IOException; import java.io.Serial; import java.io.Serializable; +import java.net.URL; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -78,7 +81,14 @@ public class BuildData implements Action, Serializable, Cloneable { public Set remoteUrls = new HashSet<>(); /** - * Allow disambiguation of the action url when multiple {@link BuildData} actions present. + * The Git console browser (if any) used to link to the repository. + */ + @CheckForNull + public GitRepositoryBrowser browser; + + /** + * Allow disambiguation of the action url when multiple {@link BuildData} + * actions present. */ @CheckForNull private Integer index; @@ -288,6 +298,65 @@ public void addRemoteUrl(String remoteUrl) { remoteUrls.add(remoteUrl); } + public void setBrowser(GitRepositoryBrowser browser) { + this.browser = browser; + } + + public GitRepositoryBrowser getBrowser() { + return browser; + } + + public String getCommitUrl(String commitId) { + if (browser == null || commitId == null) { + return null; + } + try { + URL url = browser.getChangeSetLink(commitId); + if (url == null) { + return null; + } + return url.toExternalForm().replace(".git/commit/", "/commit/"); + } catch (IOException e) { + return null; + } + } + + public String getRefUrl(String ref) { + if (browser == null || ref == null) { + return null; + } + // Normalization + if (ref.startsWith("refs/remotes/")) { + String[] parts = ref.split("/", 4); + if (parts.length == 4) { + ref = parts[3]; + } + } else if (ref.startsWith("refs/heads/")) { + ref = ref.substring(11); + } + try { + URL url = browser.getRefLink(ref); + if (url == null) { + return null; + } + return url.toExternalForm().replace(".git/tree/", "/tree/"); + } catch (IOException e) { + return null; + } + } + + public String getRepositoryUrl() { + if (browser == null) { + return null; + } + try { + URL url = browser.getUrl(); + return url == null ? null : url.toExternalForm(); + } catch (IOException e) { + return null; + } + } + @Exported public Set getRemoteUrls() { return remoteUrls; @@ -428,12 +497,13 @@ public boolean equals(Object o) { return Objects.equals(remoteUrls, that.remoteUrls) && Objects.equals(buildsByBranchName, that.buildsByBranchName) - && Objects.equals(lastBuild, that.lastBuild); + && Objects.equals(lastBuild, that.lastBuild) + && Objects.equals(browser, that.browser); } @Override public int hashCode() { - return Objects.hash(remoteUrls, buildsByBranchName, lastBuild); + return Objects.hash(remoteUrls, buildsByBranchName, lastBuild, browser); } /* Package protected for easier testing */ diff --git a/src/main/resources/hudson/plugins/git/util/BuildData/summary.jelly b/src/main/resources/hudson/plugins/git/util/BuildData/summary.jelly index 6b49d2f7c2..9e23fdaa0a 100644 --- a/src/main/resources/hudson/plugins/git/util/BuildData/summary.jelly +++ b/src/main/resources/hudson/plugins/git/util/BuildData/summary.jelly @@ -4,26 +4,48 @@ xmlns:f="/lib/form" xmlns:i="jelly:fmt"> - ${%Revision}: ${it.lastBuiltRevision.sha1.name()} + + + + + ${%Revision}: ${it.lastBuiltRevision.sha1.name()} + + + ${%Revision}: ${it.lastBuiltRevision.sha1.name()} + + +
${%SCM}: ${it.scmName}
- -
${%Repository}: ${remoteUrl} -
- -
${%Repository}: ${remoteUrl} -
+ + + +
${%Repository}: ${remoteUrl} +
+ +
${%Repository}: ${remoteUrl} +
+ +
${%Repository}: ${remoteUrl} +
+
- -
diff --git a/src/test/java/hudson/plugins/git/util/BuildDataBrowserTest.java b/src/test/java/hudson/plugins/git/util/BuildDataBrowserTest.java new file mode 100644 index 0000000000..aba642affa --- /dev/null +++ b/src/test/java/hudson/plugins/git/util/BuildDataBrowserTest.java @@ -0,0 +1,18 @@ +package hudson.plugins.git.util; + +import hudson.plugins.git.browser.GithubWeb; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class BuildDataBrowserTest { + + @Test + public void testBrowserPersistence() { + BuildData data = new BuildData(); + assertNull(data.getBrowser()); + + GithubWeb browser = new GithubWeb("https://github.com/jenkinsci/git-plugin"); + data.setBrowser(browser); + assertEquals(browser, data.getBrowser()); + } +} diff --git a/src/test/java/hudson/plugins/git/util/BuildDataTest.java b/src/test/java/hudson/plugins/git/util/BuildDataTest.java index 1938a2600f..4be4ba491a 100644 --- a/src/test/java/hudson/plugins/git/util/BuildDataTest.java +++ b/src/test/java/hudson/plugins/git/util/BuildDataTest.java @@ -5,7 +5,11 @@ import hudson.plugins.git.Branch; import hudson.plugins.git.Revision; import hudson.plugins.git.UserRemoteConfig; +import hudson.plugins.git.GitChangeSet; +import hudson.plugins.git.browser.GitRepositoryBrowser; +import java.io.IOException; +import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Random; @@ -576,4 +580,91 @@ void testHashCodeEmptyData() { emptyData.remoteUrls = null; assertEquals(emptyData.hashCode(), emptyData.hashCode()); } -} + + @Test + void testSshRepositoryLink() throws IOException { + BuildData buildData = new BuildData(); + // Add an SSH remote URL to simulate JENKINS-75285 scenario + buildData.addRemoteUrl("git@github.com:user/repo.git"); + + // Configure a browser that returns a valid HTTP URL + buildData.setBrowser(new StubGitRepositoryBrowser("https://github.com/user/repo/")); + + // Verify getRepositoryUrl returns the HTTP URL despite the SSH remote + assertEquals("https://github.com/user/repo/", buildData.getRepositoryUrl()); + } + + @Test + void testCommitUrlSanitization() throws IOException { + BuildData buildData = new BuildData(); + buildData.setBrowser(new StubGitRepositoryBrowser("https://github.com/user/repo/")); + + // Stub returns URL with .git, verify getCommitUrl strips it + assertEquals("https://github.com/user/repo/commit/123456", buildData.getCommitUrl("123456")); + } + + @Test + void testRefUrlNormalization() throws IOException { + BuildData buildData = new BuildData(); + buildData.setBrowser(new StubGitRepositoryBrowser("https://github.com/user/repo/")); + + // Test refs/remotes/origin/main -> main + assertEquals("https://github.com/user/repo/tree/main", buildData.getRefUrl("refs/remotes/origin/main")); + + // Test refs/heads/feature -> feature + assertEquals("https://github.com/user/repo/tree/feature", buildData.getRefUrl("refs/heads/feature")); + } + + /** + * Stub implementation of GitRepositoryBrowser for testing purposes. + */ + private static class StubGitRepositoryBrowser extends GitRepositoryBrowser { + private static final long serialVersionUID = 1L; + private final String baseUrl; + + StubGitRepositoryBrowser(String baseUrl) { + this.baseUrl = baseUrl; + } + + @Override + public URL getUrl() throws IOException { + return new URL(baseUrl); + } + + // Standard abstract method implementation + @Override + public URL getChangeSetLink(GitChangeSet changeSet) throws IOException { + return new URL(baseUrl.replaceAll("/$", "") + ".git/commit/" + changeSet.getId()); + } + + // Standard abstract method implementation + @Override + public URL getDiffLink(GitChangeSet.Path path) throws IOException { + return null; + } + + // Standard abstract method implementation + @Override + public URL getFileLink(GitChangeSet.Path path) throws IOException { + return null; + } + + // Custom method for BuildData (removed @Override to avoid errors if not in base class) + public URL getChangeSetLink(String changeSetId) throws IOException { + return new URL(baseUrl.replaceAll("/$", "") + ".git/commit/" + changeSetId); + } + + // Custom method for BuildData (removed @Override to avoid errors if not in base class) + public URL getRefLink(String ref) throws IOException { + return new URL(baseUrl.replaceAll("/$", "") + ".git/tree/" + ref); + } + + public URL getDiffLink(String path, String changeSetId) throws IOException { + return null; + } + + public URL getFileLink(String path) throws IOException { + return null; + } + } +} \ No newline at end of file