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 new file mode 100644 index 0000000000..6ef2e2aaf0 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitCommitDetail.java @@ -0,0 +1,77 @@ +package jenkins.plugins.git; + +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; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMRevisionAction; + +public class GitCommitDetail extends Detail { + private final GitRepositoryBrowser repositoryBrowser; + + public GitCommitDetail(Run run, GitRepositoryBrowser repositoryBrowser) { + super(run); + this.repositoryBrowser = repositoryBrowser; + } + + public String getIconClassName() { + return getDisplayName() == null ? null : "symbol-git-commit 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 && 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; + } + + @Override + public DetailGroup getGroup() { + return SCMDetailGroup.get(); + } + + private SCMRevision getRevision() { + SCMRevisionAction scmRevisionAction = getObject().getAction(SCMRevisionAction.class); + + + if (scmRevisionAction == null) { + return null; + } + + return scmRevisionAction.getRevision(); + } +} 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..72bc887f64 --- /dev/null +++ b/src/main/java/jenkins/plugins/git/GitDetailFactory.java @@ -0,0 +1,42 @@ +package jenkins.plugins.git; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Run; +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.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()); + + if (src instanceof AbstractGitSCMSource gitSource) { + SCMRevisionAction scmRevisionAction = target.getAction(SCMRevisionAction.class); + + GitRepositoryBrowser repositoryBrowser = gitSource.guessBrowser(); + + 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(); + } + } +} 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..9c15197a20 --- /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()); + + } +}