From 493799cb99aa722f41ff22fc4e14d80811bfdd0a Mon Sep 17 00:00:00 2001 From: Anshul Date: Sat, 7 Feb 2026 09:49:01 +0530 Subject: [PATCH 1/4] Fix issue [JENKINS-75285] Make SSH repository URLs clickable in build summaryi SSH repository URLs (like git@github.com:user/repo.git) were displayed as plain text in the build summary page, while HTTPS URLs were clickable. This made it difficult to quickly navigate to the repository from builds using SSH URLs. Added getRepositoryBrowserUrl() method to BuildData class that converts SSH URLs to HTTP URLs using the configured Git browser. Updated the summary.jelly template to use this method for creating hyperlinks. When a Git browser is configured, SSH URLs are now converted to their corresponding web viewer URLs and displayed as clickable links, matching the existing behavior for HTTPS URLs. If no browser is configured, the original URL is displayed as before. --- .../hudson/plugins/git/util/BuildData.java | 62 +++++++++++++++++-- .../plugins/git/util/BuildData/summary.jelly | 53 ++++++++-------- .../plugins/git/util/BuildDataTest.java | 43 +++++++++++++ 3 files changed, 129 insertions(+), 29 deletions(-) diff --git a/src/main/java/hudson/plugins/git/util/BuildData.java b/src/main/java/hudson/plugins/git/util/BuildData.java index 63c44ea57e..06491e12aa 100644 --- a/src/main/java/hudson/plugins/git/util/BuildData.java +++ b/src/main/java/hudson/plugins/git/util/BuildData.java @@ -2,11 +2,9 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import hudson.model.AbstractBuild; -import hudson.model.Action; -import hudson.model.Api; -import hudson.model.Run; +import hudson.model.*; import hudson.plugins.git.Branch; +import hudson.plugins.git.GitSCM; import hudson.plugins.git.Revision; import hudson.plugins.git.UserRemoteConfig; @@ -438,4 +436,60 @@ public int hashCode() { /* Package protected for easier testing */ static final Logger LOGGER = Logger.getLogger(BuildData.class.getName()); + + + + /** + * Get the repository browser URL for a given remote URL. + * Converts SSH URLs to HTTP URLs using the configured Git browser. + * + * @param remoteUrl the remote repository URL (may be SSH or HTTP) + * @return the browser URL if available, otherwise the original remote URL + */ + @Restricted(NoExternalUse.class) + public String getRepositoryBrowserUrl(String remoteUrl) { + if (remoteUrl == null) { + return null; + } + + Run run = getOwningRun(); + if (run == null) { + return remoteUrl; + } + + // Try to get GitSCM from the project + GitSCM gitScm = getGitSCM(run); + if (gitScm != null && gitScm.getBrowser() != null) { + try { + String browserUrl = gitScm.getBrowser().getRepoUrl(); + if (browserUrl != null && !browserUrl.isEmpty()) { + return browserUrl; + } + } catch (Exception e) { + LOGGER.log(Level.FINE, "Failed to get browser URL for " + remoteUrl, e); + } + } + + // If no browser configured or error, return original URL + return remoteUrl; + } + + /** + * Get the GitSCM instance from the run. + * + * @param run the current run + * @return GitSCM instance or null if not found + */ + private GitSCM getGitSCM(Run run) { + if (run instanceof AbstractBuild) { + AbstractBuild build = (AbstractBuild) run; + AbstractProject project = build.getProject(); + if (project != null && project.getScm() instanceof GitSCM) { + return (GitSCM) project.getScm(); + } + } + return null; + } + } + 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..de8dcc0544 100644 --- a/src/main/resources/hudson/plugins/git/util/BuildData/summary.jelly +++ b/src/main/resources/hudson/plugins/git/util/BuildData/summary.jelly @@ -1,29 +1,32 @@ + xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" + xmlns:f="/lib/form" xmlns:i="jelly:fmt"> - - ${%Revision}: ${it.lastBuiltRevision.sha1.name()} -
${%SCM}: ${it.scmName}
- - - -
${%Repository}: ${remoteUrl} -
- -
${%Repository}: ${remoteUrl} -
-
-
-
    - - -
  • ${branch.name}
  • -
    -
    -
- - + ${%Revision}: ${it.lastBuiltRevision.sha1.name()} +
${%SCM}: ${it.scmName}
+ + + + + +
${%Repository}: ${remoteUrl} +
+ +
${%Repository}: ${remoteUrl} +
+ +
${%Repository}: ${remoteUrl} +
+
+
+
+
    + + +
  • ${branch.name}
  • +
    +
    +
-
+ \ No newline at end of file diff --git a/src/test/java/hudson/plugins/git/util/BuildDataTest.java b/src/test/java/hudson/plugins/git/util/BuildDataTest.java index 1938a2600f..c9ca201b51 100644 --- a/src/test/java/hudson/plugins/git/util/BuildDataTest.java +++ b/src/test/java/hudson/plugins/git/util/BuildDataTest.java @@ -576,4 +576,47 @@ void testHashCodeEmptyData() { emptyData.remoteUrls = null; assertEquals(emptyData.hashCode(), emptyData.hashCode()); } + + + + @Test + void testGetRepositoryBrowserUrlWithNullUrl() { + String result = data.getRepositoryBrowserUrl(null); + assertNull(result, "Null URL should return null"); + } + + @Test + void testGetRepositoryBrowserUrlWithoutOwningRun() { + // When there's no owning run, should return the original URL + String sshUrl = "git@github.com:jenkinsci/git-plugin.git"; + String result = data.getRepositoryBrowserUrl(sshUrl); + assertThat(result, is(sshUrl)); + } + + @Test + void testGetRepositoryBrowserUrlWithHttpsUrl() { + // HTTPS URLs should be returned as-is when no run context + String httpsUrl = "https://github.com/jenkinsci/git-plugin.git"; + String result = data.getRepositoryBrowserUrl(httpsUrl); + assertThat(result, is(httpsUrl)); + } + + @Test + void testGetRepositoryBrowserUrlWithSshUrl() { + // SSH URLs should be returned as-is when no run context + String sshUrl = "git@github.com:jenkinsci/git-plugin.git"; + String result = data.getRepositoryBrowserUrl(sshUrl); + assertThat(result, is(sshUrl)); + } + + @Test + void testGetGitSCMReturnsNullWithoutRun() { + // getGitSCM should return null when there's no owning run + // This tests the private method indirectly through getRepositoryBrowserUrl + String testUrl = "git@github.com:jenkinsci/git-plugin.git"; + String result = data.getRepositoryBrowserUrl(testUrl); + // Should return original URL since there's no SCM to get browser from + assertThat(result, is(testUrl)); + } + } From d30b28a35aae93fdfd5769cbc35f2d097ef4d4c5 Mon Sep 17 00:00:00 2001 From: Anshul Date: Mon, 9 Feb 2026 10:41:56 +0530 Subject: [PATCH 2/4] Address Copilot review feedback - Add @CheckForNull annotations for nullability clarity - Apply fix to both summary.jelly and index.jelly for consistency - Add null safety check in getGitSCM method - Remove basic unit tests - will rely on manual testing --- .../hudson/plugins/git/util/BuildData.java | 83 ++++++++++++++----- .../plugins/git/util/BuildData/index.jelly | 57 +++++++------ .../plugins/git/util/BuildData/summary.jelly | 5 +- .../plugins/git/util/BuildDataTest.java | 38 --------- 4 files changed, 94 insertions(+), 89 deletions(-) diff --git a/src/main/java/hudson/plugins/git/util/BuildData.java b/src/main/java/hudson/plugins/git/util/BuildData.java index 06491e12aa..345f90d02e 100644 --- a/src/main/java/hudson/plugins/git/util/BuildData.java +++ b/src/main/java/hudson/plugins/git/util/BuildData.java @@ -441,46 +441,73 @@ public int hashCode() { /** * Get the repository browser URL for a given remote URL. - * Converts SSH URLs to HTTP URLs using the configured Git browser. + * For HTTP(S) URLs, returns the URL itself (already clickable). + * For SSH URLs, attempts to convert using the configured Git browser. * - * @param remoteUrl the remote repository URL (may be SSH or HTTP) - * @return the browser URL if available, otherwise the original remote URL + * @param remoteUrl the remote repository URL (may be SSH or HTTP, may be {@code null}) + * @return the browser URL if available, otherwise the original remote URL; may be {@code null} if {@code remoteUrl} is {@code null} */ @Restricted(NoExternalUse.class) - public String getRepositoryBrowserUrl(String remoteUrl) { - if (remoteUrl == null) { + @CheckForNull + public String getRepositoryBrowserUrl(@CheckForNull String remoteUrl) { + if (remoteUrl == null || remoteUrl.isEmpty()) { return null; } + // If already HTTP(S), it's clickable as-is + if (remoteUrl.startsWith("http://") || remoteUrl.startsWith("https://")) { + return remoteUrl; + } + + // For SSH URLs, try to get browser URL Run run = getOwningRun(); if (run == null) { - return remoteUrl; + return null; } - // Try to get GitSCM from the project GitSCM gitScm = getGitSCM(run); - if (gitScm != null && gitScm.getBrowser() != null) { - try { - String browserUrl = gitScm.getBrowser().getRepoUrl(); - if (browserUrl != null && !browserUrl.isEmpty()) { - return browserUrl; - } - } catch (Exception e) { - LOGGER.log(Level.FINE, "Failed to get browser URL for " + remoteUrl, e); + if (gitScm == null || gitScm.getBrowser() == null) { + return null; + } + + // Check if this remoteUrl is configured in the SCM + boolean isConfiguredRemote = gitScm.getUserRemoteConfigs().stream() + .anyMatch(config -> config.getUrl() != null && + (config.getUrl().equals(remoteUrl) || + normalize(config.getUrl()).equals(normalize(remoteUrl)))); + + if (!isConfiguredRemote) { + return null; + } + + // Return the browser URL + try { + String browserUrl = gitScm.getBrowser().getRepoUrl(); + if (browserUrl != null && !browserUrl.isEmpty() && + (browserUrl.startsWith("http://") || browserUrl.startsWith("https://"))) { + return browserUrl; } + } catch (Exception e) { + LOGGER.log(Level.FINE, "Failed to get browser URL for " + remoteUrl, e); } - // If no browser configured or error, return original URL - return remoteUrl; + return null; } /** * Get the GitSCM instance from the run. + * Supports both AbstractBuild (Freestyle) and WorkflowRun (Pipeline) jobs. * * @param run the current run * @return GitSCM instance or null if not found */ - private GitSCM getGitSCM(Run run) { + @CheckForNull + private GitSCM getGitSCM(@CheckForNull Run run) { + if (run == null) { + return null; + } + + // Try AbstractBuild (Freestyle jobs) if (run instanceof AbstractBuild) { AbstractBuild build = (AbstractBuild) run; AbstractProject project = build.getProject(); @@ -488,8 +515,24 @@ private GitSCM getGitSCM(Run run) { return (GitSCM) project.getScm(); } } + + // Try WorkflowRun (Pipeline jobs) + if (run.getParent() != null) { + Object parent = run.getParent(); + + // Use reflection to check for getSCM() method + try { + java.lang.reflect.Method getScmMethod = parent.getClass().getMethod("getScm"); + Object scm = getScmMethod.invoke(parent); + if (scm instanceof GitSCM) { + return (GitSCM) scm; + } + } catch (Exception e) { + LOGGER.log(Level.FINEST, "Could not get SCM via getSCM() method", e); + } + } + return null; } -} - +} \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/git/util/BuildData/index.jelly b/src/main/resources/hudson/plugins/git/util/BuildData/index.jelly index 7684a80e6f..252b3281f1 100644 --- a/src/main/resources/hudson/plugins/git/util/BuildData/index.jelly +++ b/src/main/resources/hudson/plugins/git/util/BuildData/index.jelly @@ -5,35 +5,38 @@ - -

${%Git Build Data}

+ +

${%Git Build Data}

- ${%Revision}: ${it.lastBuild.SHA1.name()} -
${%SCM}: ${it.scmName}
- - - -
${%Repository}: ${remoteUrl} + ${%Revision}: ${it.lastBuild.SHA1.name()} +
${%SCM}: ${it.scmName}
+ + + + + +
${%Repository}: ${remoteUrl} +
+ +
${%Repository}: ${remoteUrl} +
+
+
- -
${%Repository}: ${remoteUrl} -
-
-
-
    - -
  • ${branch.name}
  • -
    -
+
    + +
  • ${branch.name}
  • +
    +
-

${%Built Branches}

-
    +

    ${%Built Branches}

    +
      - -
    • (unnamed)${branch}: ${it.buildsByBranchName.get(branch).toString()}
    • -
      -
    + +
  • (unnamed)${branch}: ${it.buildsByBranchName.get(branch).toString()}
  • +
    +
-
- - +
+ + \ No newline at end of file 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 de8dcc0544..d9c7de161f 100644 --- a/src/main/resources/hudson/plugins/git/util/BuildData/summary.jelly +++ b/src/main/resources/hudson/plugins/git/util/BuildData/summary.jelly @@ -9,12 +9,9 @@ - +
${%Repository}: ${remoteUrl}
- -
${%Repository}: ${remoteUrl} -

${%Repository}: ${remoteUrl}
diff --git a/src/test/java/hudson/plugins/git/util/BuildDataTest.java b/src/test/java/hudson/plugins/git/util/BuildDataTest.java index c9ca201b51..c5b5d17dfb 100644 --- a/src/test/java/hudson/plugins/git/util/BuildDataTest.java +++ b/src/test/java/hudson/plugins/git/util/BuildDataTest.java @@ -579,44 +579,6 @@ void testHashCodeEmptyData() { - @Test - void testGetRepositoryBrowserUrlWithNullUrl() { - String result = data.getRepositoryBrowserUrl(null); - assertNull(result, "Null URL should return null"); - } - @Test - void testGetRepositoryBrowserUrlWithoutOwningRun() { - // When there's no owning run, should return the original URL - String sshUrl = "git@github.com:jenkinsci/git-plugin.git"; - String result = data.getRepositoryBrowserUrl(sshUrl); - assertThat(result, is(sshUrl)); - } - - @Test - void testGetRepositoryBrowserUrlWithHttpsUrl() { - // HTTPS URLs should be returned as-is when no run context - String httpsUrl = "https://github.com/jenkinsci/git-plugin.git"; - String result = data.getRepositoryBrowserUrl(httpsUrl); - assertThat(result, is(httpsUrl)); - } - - @Test - void testGetRepositoryBrowserUrlWithSshUrl() { - // SSH URLs should be returned as-is when no run context - String sshUrl = "git@github.com:jenkinsci/git-plugin.git"; - String result = data.getRepositoryBrowserUrl(sshUrl); - assertThat(result, is(sshUrl)); - } - - @Test - void testGetGitSCMReturnsNullWithoutRun() { - // getGitSCM should return null when there's no owning run - // This tests the private method indirectly through getRepositoryBrowserUrl - String testUrl = "git@github.com:jenkinsci/git-plugin.git"; - String result = data.getRepositoryBrowserUrl(testUrl); - // Should return original URL since there's no SCM to get browser from - assertThat(result, is(testUrl)); - } } From 08de10b8bda13280df1f6d1c1d60c38af81c94de Mon Sep 17 00:00:00 2001 From: Anshul Date: Mon, 9 Feb 2026 11:04:10 +0530 Subject: [PATCH 3/4] Updated the BuildData file to address the spotbugs --- .../hudson/plugins/git/util/BuildData.java | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/src/main/java/hudson/plugins/git/util/BuildData.java b/src/main/java/hudson/plugins/git/util/BuildData.java index 345f90d02e..0f7ede892b 100644 --- a/src/main/java/hudson/plugins/git/util/BuildData.java +++ b/src/main/java/hudson/plugins/git/util/BuildData.java @@ -10,13 +10,9 @@ import java.io.Serial; import java.io.Serializable; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.lang.reflect.InvocationTargetException; +import java.util.*; + import org.eclipse.jgit.lib.ObjectId; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -438,7 +434,6 @@ public int hashCode() { static final Logger LOGGER = Logger.getLogger(BuildData.class.getName()); - /** * Get the repository browser URL for a given remote URL. * For HTTP(S) URLs, returns the URL itself (already clickable). @@ -471,24 +466,30 @@ public String getRepositoryBrowserUrl(@CheckForNull String remoteUrl) { } // Check if this remoteUrl is configured in the SCM - boolean isConfiguredRemote = gitScm.getUserRemoteConfigs().stream() - .anyMatch(config -> config.getUrl() != null && - (config.getUrl().equals(remoteUrl) || - normalize(config.getUrl()).equals(normalize(remoteUrl)))); + List remoteConfigs = gitScm.getUserRemoteConfigs(); + if (remoteConfigs == null || remoteConfigs.isEmpty()) { + return null; + } + + boolean isConfiguredRemote = remoteConfigs.stream() + .anyMatch(config -> { + String configUrl = config.getUrl(); + if (configUrl == null) { + return false; + } + return configUrl.equals(remoteUrl) || + normalize(configUrl).equals(normalize(remoteUrl)); + }); if (!isConfiguredRemote) { return null; } // Return the browser URL - try { - String browserUrl = gitScm.getBrowser().getRepoUrl(); - if (browserUrl != null && !browserUrl.isEmpty() && - (browserUrl.startsWith("http://") || browserUrl.startsWith("https://"))) { - return browserUrl; - } - } catch (Exception e) { - LOGGER.log(Level.FINE, "Failed to get browser URL for " + remoteUrl, e); + String browserUrl = gitScm.getBrowser().getRepoUrl(); + if (browserUrl != null && !browserUrl.isEmpty() && + (browserUrl.startsWith("http://") || browserUrl.startsWith("https://"))) { + return browserUrl; } return null; @@ -517,19 +518,24 @@ private GitSCM getGitSCM(@CheckForNull Run run) { } // Try WorkflowRun (Pipeline jobs) - if (run.getParent() != null) { - Object parent = run.getParent(); + Object parent = run.getParent(); + if (parent == null) { + return null; + } - // Use reflection to check for getSCM() method - try { - java.lang.reflect.Method getScmMethod = parent.getClass().getMethod("getScm"); - Object scm = getScmMethod.invoke(parent); - if (scm instanceof GitSCM) { - return (GitSCM) scm; - } - } catch (Exception e) { - LOGGER.log(Level.FINEST, "Could not get SCM via getSCM() method", e); + // Use reflection to check for getSCM() method + try { + java.lang.reflect.Method getScmMethod = parent.getClass().getMethod("getScm"); + Object scm = getScmMethod.invoke(parent); + if (scm instanceof GitSCM) { + return (GitSCM) scm; } + } catch (NoSuchMethodException e) { + LOGGER.log(Level.FINEST, "getSCM() method not found", e); + } catch (IllegalAccessException e) { + LOGGER.log(Level.FINEST, "Could not invoke getSCM() method", e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); } return null; From de77fa7ad8bb66a0d615e5a4f69bb3abdc104628 Mon Sep 17 00:00:00 2001 From: Anshul Date: Tue, 10 Feb 2026 10:40:38 +0530 Subject: [PATCH 4/4] Fix Spotbugs warnings --- .../hudson/plugins/git/util/BuildData.java | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/main/java/hudson/plugins/git/util/BuildData.java b/src/main/java/hudson/plugins/git/util/BuildData.java index 0f7ede892b..05b0527e42 100644 --- a/src/main/java/hudson/plugins/git/util/BuildData.java +++ b/src/main/java/hudson/plugins/git/util/BuildData.java @@ -13,6 +13,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.*; +import hudson.plugins.git.browser.GitRepositoryBrowser; import org.eclipse.jgit.lib.ObjectId; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -434,14 +435,6 @@ public int hashCode() { static final Logger LOGGER = Logger.getLogger(BuildData.class.getName()); - /** - * Get the repository browser URL for a given remote URL. - * For HTTP(S) URLs, returns the URL itself (already clickable). - * For SSH URLs, attempts to convert using the configured Git browser. - * - * @param remoteUrl the remote repository URL (may be SSH or HTTP, may be {@code null}) - * @return the browser URL if available, otherwise the original remote URL; may be {@code null} if {@code remoteUrl} is {@code null} - */ @Restricted(NoExternalUse.class) @CheckForNull public String getRepositoryBrowserUrl(@CheckForNull String remoteUrl) { @@ -471,22 +464,41 @@ public String getRepositoryBrowserUrl(@CheckForNull String remoteUrl) { return null; } - boolean isConfiguredRemote = remoteConfigs.stream() - .anyMatch(config -> { - String configUrl = config.getUrl(); - if (configUrl == null) { - return false; - } - return configUrl.equals(remoteUrl) || - normalize(configUrl).equals(normalize(remoteUrl)); - }); + String normalizedRemoteUrl = normalize(remoteUrl); + + boolean isConfiguredRemote = false; + for (UserRemoteConfig config : remoteConfigs) { + String configUrl = config.getUrl(); + if (configUrl == null) { + continue; + } + + // Direct match + if (configUrl.equals(remoteUrl)) { + isConfiguredRemote = true; + break; + } + + // Normalized match (only if both normalize successfully) + if (normalizedRemoteUrl != null) { + String normalizedConfigUrl = normalize(configUrl); + if (normalizedConfigUrl != null && normalizedConfigUrl.equals(normalizedRemoteUrl)) { + isConfiguredRemote = true; + break; + } + } + } if (!isConfiguredRemote) { return null; } - // Return the browser URL - String browserUrl = gitScm.getBrowser().getRepoUrl(); + GitRepositoryBrowser browser = gitScm.getBrowser(); + if (browser == null) { + return null; + } + + String browserUrl = browser.getRepoUrl(); if (browserUrl != null && !browserUrl.isEmpty() && (browserUrl.startsWith("http://") || browserUrl.startsWith("https://"))) { return browserUrl; @@ -517,11 +529,8 @@ private GitSCM getGitSCM(@CheckForNull Run run) { } } - // Try WorkflowRun (Pipeline jobs) + // Try WorkflowRun (Pipeline jobs) - parent is always non-null for Run Object parent = run.getParent(); - if (parent == null) { - return null; - } // Use reflection to check for getSCM() method try { @@ -532,10 +541,10 @@ private GitSCM getGitSCM(@CheckForNull Run run) { } } catch (NoSuchMethodException e) { LOGGER.log(Level.FINEST, "getSCM() method not found", e); - } catch (IllegalAccessException e) { + } catch (IllegalAccessException e) { LOGGER.log(Level.FINEST, "Could not invoke getSCM() method", e); } catch (InvocationTargetException e) { - throw new RuntimeException(e); + LOGGER.log(Level.FINEST, "Error invoking getSCM() method", e); } return null;