From f9911cb8dd9d0f6f509dfa91b0cb27e52695df57 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Thu, 16 Oct 2025 10:01:08 +0100 Subject: [PATCH 1/5] Add Git Detail --- .../jenkins/plugins/git/GitCommitDetail.java | 65 +++++++++++++++++++ .../jenkins/plugins/git/GitDetailFactory.java | 41 ++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 src/main/java/jenkins/plugins/git/GitCommitDetail.java create mode 100644 src/main/java/jenkins/plugins/git/GitDetailFactory.java diff --git a/src/main/java/jenkins/plugins/git/GitCommitDetail.java b/src/main/java/jenkins/plugins/git/GitCommitDetail.java new file mode 100644 index 0000000000..1697533b37 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitCommitDetail.java @@ -0,0 +1,65 @@ +package jenkins.plugins.git; + +import hudson.model.Actionable; +import hudson.model.Run; +import jenkins.model.details.Detail; +import jenkins.model.details.DetailGroup; +import jenkins.scm.api.SCMDetailGroup; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMRevisionAction; + +public class GitCommitDetail extends Detail { + public GitCommitDetail(Run run) { + super(run); + } + + public String getIconClassName() { + return "symbol-git-commit-outline plugin-ionicons-api"; + } + + @Override + public String getDisplayName() { + SCMRevision revision = getRevision(); + + if (revision == null) { + return null; + } + + if (revision instanceof AbstractGitSCMSource.SCMRevisionImpl abstractRevision) { + return abstractRevision.getHash().substring(0, 7); + } + + return null; + } + + @Override + public String getLink() { + SCMRevision revision = getRevision(); + + if (revision == null) { + return null; + } + +// if (revision instanceof AbstractGitSCMSource.SCMRevisionImpl abstractRevision) { +// return new GitHubRepositoryDetail(getObject()).getLink() + "/commit/" + abstractRevision.getHash(); +// } + + return null; + } + + @Override + public DetailGroup getGroup() { + return SCMDetailGroup.get(); + } + + private SCMRevision getRevision() { + SCMRevisionAction scmRevisionAction = getObject().getAction(SCMRevisionAction.class); + + + if (scmRevisionAction == null) { + return null; + } + + return scmRevisionAction.getRevision(); + } +} \ No newline at end of file diff --git a/src/main/java/jenkins/plugins/git/GitDetailFactory.java b/src/main/java/jenkins/plugins/git/GitDetailFactory.java new file mode 100644 index 0000000000..97a9a05fb3 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitDetailFactory.java @@ -0,0 +1,41 @@ +package jenkins.plugins.git; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Run; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import jenkins.model.details.Detail; +import jenkins.model.details.DetailFactory; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMRevisionAction; +import jenkins.scm.api.SCMSource; + +@Extension +public final class GitDetailFactory extends DetailFactory { + + @Override + public Class type() { + return Run.class; + } + + @NonNull + @Override + public List createFor(@NonNull Run target) { + SCMSource src = SCMSource.SourceByItem.findSource(target.getParent()); + + // Don't add details for non-Git SCM sources + if (!(src instanceof AbstractGitSCMSource)) { + return Collections.emptyList(); + } + + SCMRevisionAction scmRevisionAction = target.getAction(SCMRevisionAction.class); + + if (scmRevisionAction == null) { + return Collections.emptyList(); + } + + return List.of(new GitCommitDetail(target)); + } +} \ No newline at end of file From 7c81e3c96b72fee321bb0155bbbeb9716eb1246e Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Thu, 16 Oct 2025 11:57:15 +0100 Subject: [PATCH 2/5] Retrieve from repository web --- src/main/java/hudson/plugins/git/GitSCM.java | 29 +++++++++++------- .../plugins/git/AbstractGitSCMSource.java | 30 +++++++++++++++++++ .../jenkins/plugins/git/GitCommitDetail.java | 24 +++++++++++---- .../jenkins/plugins/git/GitDetailFactory.java | 21 ++++++------- 4 files changed, 77 insertions(+), 27 deletions(-) diff --git a/src/main/java/hudson/plugins/git/GitSCM.java b/src/main/java/hudson/plugins/git/GitSCM.java index 9b609fcbfd..d3bb4ab1d8 100644 --- a/src/main/java/hudson/plugins/git/GitSCM.java +++ b/src/main/java/hudson/plugins/git/GitSCM.java @@ -64,6 +64,8 @@ import org.jenkinsci.plugins.gitclient.*; import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.StaplerRequest2; @@ -368,7 +370,7 @@ public void setBrowser(GitRepositoryBrowser browser) { + "/*" // optional trailing '/' ; - private static final Pattern[] URL_PATTERNS = { + public static final Pattern[] URL_PATTERNS = { /* URL style - like https://github.com/jenkinsci/git-plugin */ Pattern.compile( "(?:\\w+://)" // protocol (scheme) @@ -407,21 +409,26 @@ public void setBrowser(GitRepositoryBrowser browser) { } if (webUrls.size() == 1) { String url = webUrls.iterator().next(); - if (url.startsWith("https://bitbucket.org/")) { - return new BitbucketWeb(url); - } - if (url.startsWith("https://gitlab.com/")) { - return new GitLab(url); - } - if (url.startsWith("https://github.com/")) { - return new GithubWeb(url); - } - return null; + return guessBrowser(url); } LOGGER.log(Level.INFO, "Multiple browser guess matches for {0}", remoteRepositories); return null; } + @Restricted(NoExternalUse.class) + public static GitRepositoryBrowser guessBrowser(String url) { + if (url.startsWith("https://bitbucket.org/")) { + return new BitbucketWeb(url); + } + if (url.startsWith("https://gitlab.com/")) { + return new GitLab(url); + } + if (url.startsWith("https://github.com/")) { + return new GithubWeb(url); + } + return null; + } + public boolean isCreateAccountBasedOnEmail() { DescriptorImpl gitDescriptor = getDescriptor(); return (gitDescriptor != null && gitDescriptor.isCreateAccountBasedOnEmail()); diff --git a/src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java b/src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java index 1504297965..52b7536756 100644 --- a/src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java +++ b/src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java @@ -53,6 +53,7 @@ import hudson.plugins.git.util.BuildChooserContext; import hudson.plugins.git.util.BuildData; import hudson.plugins.git.util.GitUtils; +import hudson.scm.RepositoryBrowser; import hudson.scm.SCM; import hudson.security.ACL; import java.io.File; @@ -75,6 +76,7 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Matcher; import java.util.regex.Pattern; import jenkins.model.Jenkins; import jenkins.plugins.git.traits.GitBrowserSCMSourceTrait; @@ -223,6 +225,34 @@ public GitRepositoryBrowser getBrowser() { return trait != null ? trait.getBrowser() : null; } + @CheckForNull + public GitRepositoryBrowser guessBrowser() { + GitBrowserSCMSourceTrait trait = SCMTrait.find(getTraits(), GitBrowserSCMSourceTrait.class); + if (trait != null) { + return trait.getBrowser(); + } + + Set webUrls = new HashSet<>(); + String remote = getRemote(); + if (remote != null) { + for (Pattern p : GitSCM.URL_PATTERNS) { + Matcher m = p.matcher(remote); + if (m.matches()) { + webUrls.add("https://" + m.group(1) + "/" + m.group(2) + "/"); + } + } + } + if (webUrls.isEmpty()) { + return null; + } + if (webUrls.size() == 1) { + String url = webUrls.iterator().next(); + return GitSCM.guessBrowser(url); + } + LOGGER.log(Level.INFO, "Multiple browser guess matches for {0}", remote); + return null; + } + /** * Gets Git tool to be used for this SCM Source. * @return Git Tool or {@code null} if the default tool should be used. diff --git a/src/main/java/jenkins/plugins/git/GitCommitDetail.java b/src/main/java/jenkins/plugins/git/GitCommitDetail.java index 1697533b37..6078fe5cbb 100644 --- a/src/main/java/jenkins/plugins/git/GitCommitDetail.java +++ b/src/main/java/jenkins/plugins/git/GitCommitDetail.java @@ -1,7 +1,9 @@ package jenkins.plugins.git; -import hudson.model.Actionable; import hudson.model.Run; +import hudson.plugins.git.browser.GitRepositoryBrowser; +import java.io.IOException; +import java.net.URL; import jenkins.model.details.Detail; import jenkins.model.details.DetailGroup; import jenkins.scm.api.SCMDetailGroup; @@ -9,12 +11,15 @@ import jenkins.scm.api.SCMRevisionAction; public class GitCommitDetail extends Detail { - public GitCommitDetail(Run run) { + private final GitRepositoryBrowser repositoryBrowser; + + public GitCommitDetail(Run run, GitRepositoryBrowser repositoryBrowser) { super(run); + this.repositoryBrowser = repositoryBrowser; } public String getIconClassName() { - return "symbol-git-commit-outline plugin-ionicons-api"; + return getDisplayName() == null ? null : "symbol-git-commit plugin-ionicons-api"; } @Override @@ -40,9 +45,16 @@ public String getLink() { return null; } -// if (revision instanceof AbstractGitSCMSource.SCMRevisionImpl abstractRevision) { -// return new GitHubRepositoryDetail(getObject()).getLink() + "/commit/" + abstractRevision.getHash(); -// } + if (revision instanceof AbstractGitSCMSource.SCMRevisionImpl abstractRevision && repositoryBrowser != null) { + String hash = abstractRevision.getHash(); + + try { + URL changeSetLink = repositoryBrowser.getChangeSetLink(hash); + return changeSetLink != null ? changeSetLink.toString() : null; + } catch (IOException e) { + return null; + } + } return null; } diff --git a/src/main/java/jenkins/plugins/git/GitDetailFactory.java b/src/main/java/jenkins/plugins/git/GitDetailFactory.java index 97a9a05fb3..38cda30d7c 100644 --- a/src/main/java/jenkins/plugins/git/GitDetailFactory.java +++ b/src/main/java/jenkins/plugins/git/GitDetailFactory.java @@ -3,12 +3,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.model.Run; -import java.util.ArrayList; +import hudson.plugins.git.browser.GitRepositoryBrowser; import java.util.Collections; import java.util.List; import jenkins.model.details.Detail; import jenkins.model.details.DetailFactory; -import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMRevisionAction; import jenkins.scm.api.SCMSource; @@ -25,17 +24,19 @@ public Class type() { public List createFor(@NonNull Run target) { SCMSource src = SCMSource.SourceByItem.findSource(target.getParent()); - // Don't add details for non-Git SCM sources - if (!(src instanceof AbstractGitSCMSource)) { - return Collections.emptyList(); - } + if (src instanceof AbstractGitSCMSource gitSource) { + SCMRevisionAction scmRevisionAction = target.getAction(SCMRevisionAction.class); - SCMRevisionAction scmRevisionAction = target.getAction(SCMRevisionAction.class); + GitRepositoryBrowser repositoryBrowser = gitSource.guessBrowser(); - if (scmRevisionAction == null) { + if (scmRevisionAction == null) { + return Collections.emptyList(); + } + + return List.of(new GitCommitDetail(target, repositoryBrowser)); + } else { + // Don't add details for non-Git SCM sources return Collections.emptyList(); } - - return List.of(new GitCommitDetail(target)); } } \ No newline at end of file From dbd58806a60a9f1139e4dbafd8869604f8107dde Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Thu, 16 Oct 2025 12:03:51 +0100 Subject: [PATCH 3/5] New lines --- src/main/java/jenkins/plugins/git/GitCommitDetail.java | 2 +- src/main/java/jenkins/plugins/git/GitDetailFactory.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/jenkins/plugins/git/GitCommitDetail.java b/src/main/java/jenkins/plugins/git/GitCommitDetail.java index 6078fe5cbb..6ef2e2aaf0 100644 --- a/src/main/java/jenkins/plugins/git/GitCommitDetail.java +++ b/src/main/java/jenkins/plugins/git/GitCommitDetail.java @@ -74,4 +74,4 @@ private SCMRevision getRevision() { return scmRevisionAction.getRevision(); } -} \ No newline at end of file +} diff --git a/src/main/java/jenkins/plugins/git/GitDetailFactory.java b/src/main/java/jenkins/plugins/git/GitDetailFactory.java index 38cda30d7c..72bc887f64 100644 --- a/src/main/java/jenkins/plugins/git/GitDetailFactory.java +++ b/src/main/java/jenkins/plugins/git/GitDetailFactory.java @@ -39,4 +39,4 @@ public List createFor(@NonNull Run target) { return Collections.emptyList(); } } -} \ No newline at end of file +} From 899bfa6e7f8fdd9e761b2175bd459796e3ba0a4c Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Thu, 16 Oct 2025 14:54:07 +0100 Subject: [PATCH 4/5] Add a bit of test coverage --- .../plugins/git/GitCommitDetailTest.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/test/java/jenkins/plugins/git/GitCommitDetailTest.java diff --git a/src/test/java/jenkins/plugins/git/GitCommitDetailTest.java b/src/test/java/jenkins/plugins/git/GitCommitDetailTest.java new file mode 100644 index 0000000000..4023bd452f --- /dev/null +++ b/src/test/java/jenkins/plugins/git/GitCommitDetailTest.java @@ -0,0 +1,30 @@ +package jenkins.plugins.git; + +import hudson.model.Result; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; + +import static org.junit.jupiter.api.Assertions.assertNull; + +@WithJenkins +class GitCommitDetailTest { + + @Test + void testIsHiddenIfNoScm(JenkinsRule j) throws Exception { + WorkflowJob project = j.createProject(WorkflowJob.class); + project.setDefinition(new CpsFlowDefinition(""" + echo 'hello world' + """, true)); + + WorkflowRun workflowRun = j.buildAndAssertStatus(Result.SUCCESS, project); + + GitCommitDetail gitCommitDetail = new GitCommitDetail(workflowRun, null); + + assertNull(gitCommitDetail.getIconClassName()); + + } +} \ No newline at end of file From 8ab06a6340da2bc9a246911028c16470d3b70208 Mon Sep 17 00:00:00 2001 From: Tim Jacomb <21194782+timja@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:59:37 +0100 Subject: [PATCH 5/5] Update GitCommitDetailTest.java --- src/test/java/jenkins/plugins/git/GitCommitDetailTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/jenkins/plugins/git/GitCommitDetailTest.java b/src/test/java/jenkins/plugins/git/GitCommitDetailTest.java index 4023bd452f..9c15197a20 100644 --- a/src/test/java/jenkins/plugins/git/GitCommitDetailTest.java +++ b/src/test/java/jenkins/plugins/git/GitCommitDetailTest.java @@ -27,4 +27,4 @@ void testIsHiddenIfNoScm(JenkinsRule j) throws Exception { assertNull(gitCommitDetail.getIconClassName()); } -} \ No newline at end of file +}